mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-24 04:58:45 +00:00
Improve archive restore progress tracking and UX.
This commit is contained in:
committed by
Greyson Parrelli
parent
89a0541574
commit
1f40c7ab7e
@@ -62,6 +62,8 @@ import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.signal.core.ui.compose.theme.SignalTheme
|
||||
@@ -69,7 +71,7 @@ import org.signal.core.util.concurrent.LifecycleDisposable
|
||||
import org.signal.core.util.getSerializableCompat
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.donations.StripeApi
|
||||
import org.thoughtcrime.securesms.backup.RestoreState
|
||||
import org.thoughtcrime.securesms.backup.v2.ArchiveRestoreProgress
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.verify.VerifyBackupKeyActivity
|
||||
import org.thoughtcrime.securesms.calls.YouAreAlreadyInACallSnackbar.show
|
||||
import org.thoughtcrime.securesms.calls.log.CallLogFilter
|
||||
@@ -258,13 +260,16 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
|
||||
}
|
||||
|
||||
launch {
|
||||
mainNavigationViewModel.backupStatus.collect { remainingRestoreSize ->
|
||||
val totalRestorableSize = SignalStore.backup.totalRestorableAttachmentSize
|
||||
if (SignalStore.backup.restoreState == RestoreState.RESTORING_MEDIA && remainingRestoreSize != 0L && totalRestorableSize != 0L) {
|
||||
Log.i(TAG, "Still restoring media, launching a service. Remaining restoration size: $remainingRestoreSize out of $totalRestorableSize ")
|
||||
BackupMediaRestoreService.resetTimeout()
|
||||
BackupMediaRestoreService.start(this@MainActivity, resources.getString(R.string.BackupStatus__restoring_media))
|
||||
}
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
ArchiveRestoreProgress
|
||||
.stateFlow
|
||||
.distinctUntilChangedBy { it.needRestoreMediaService() }
|
||||
.filter { it.needRestoreMediaService() }
|
||||
.collect {
|
||||
Log.i(TAG, "Still restoring media, launching a service. Remaining restoration size: ${it.remainingRestoreSize} out of ${it.totalRestoreSize} ")
|
||||
BackupMediaRestoreService.resetTimeout()
|
||||
BackupMediaRestoreService.start(this@MainActivity, resources.getString(R.string.BackupStatus__restoring_media))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,12 +7,16 @@ package org.thoughtcrime.securesms.backup
|
||||
|
||||
import org.signal.core.util.LongSerializer
|
||||
|
||||
enum class RestoreState(val id: Int, val inProgress: Boolean) {
|
||||
FAILED(-1, false),
|
||||
enum class RestoreState(private val id: Int, val inProgress: Boolean) {
|
||||
NONE(0, false),
|
||||
PENDING(1, true),
|
||||
RESTORING_DB(2, true),
|
||||
RESTORING_MEDIA(3, true);
|
||||
CALCULATING_MEDIA(4, true),
|
||||
RESTORING_MEDIA(3, true),
|
||||
CANCELING_MEDIA(5, true);
|
||||
|
||||
val isMediaRestoreOperation: Boolean
|
||||
get() = this == CALCULATING_MEDIA || this == RESTORING_MEDIA || this == CANCELING_MEDIA
|
||||
|
||||
companion object {
|
||||
val serializer: LongSerializer<RestoreState> = Serializer()
|
||||
@@ -23,14 +27,8 @@ enum class RestoreState(val id: Int, val inProgress: Boolean) {
|
||||
return data.id.toLong()
|
||||
}
|
||||
|
||||
override fun deserialize(data: Long): RestoreState {
|
||||
return when (data.toInt()) {
|
||||
FAILED.id -> FAILED
|
||||
PENDING.id -> PENDING
|
||||
RESTORING_DB.id -> RESTORING_DB
|
||||
RESTORING_MEDIA.id -> RESTORING_MEDIA
|
||||
else -> NONE
|
||||
}
|
||||
override fun deserialize(input: Long): RestoreState {
|
||||
return entries.firstOrNull { it.id == input.toInt() } ?: throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,22 +5,239 @@
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.net.ConnectivityManager
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import org.signal.core.util.bytes
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.throttleLatest
|
||||
import org.thoughtcrime.securesms.BuildConfig
|
||||
import org.thoughtcrime.securesms.attachments.AttachmentId
|
||||
import org.thoughtcrime.securesms.backup.RestoreState
|
||||
import org.thoughtcrime.securesms.database.DatabaseObserver
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.BatteryNotLowConstraint
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.WifiConstraint
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.util.safeUnregisterReceiver
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import kotlin.concurrent.withLock
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import kotlin.time.DurationUnit
|
||||
|
||||
/**
|
||||
* A class for tracking restore progress, largely just for debugging purposes. It keeps no state on disk, and is therefore only useful for testing.
|
||||
* A class for tracking restore progress as a whole, but with a primary focus on managing media restore.
|
||||
*
|
||||
* Also provides helpful debugging information for attachment download speeds.
|
||||
*/
|
||||
object ArchiveRestoreProgress {
|
||||
private val TAG = Log.tag(ArchiveRestoreProgress::class.java)
|
||||
|
||||
private var listenersRegistered = false
|
||||
private val listenerLock = ReentrantLock()
|
||||
|
||||
private val attachmentObserver = DatabaseObserver.Observer {
|
||||
update()
|
||||
}
|
||||
|
||||
private val networkChangeReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
update()
|
||||
}
|
||||
}
|
||||
|
||||
private val store = MutableStateFlow(
|
||||
ArchiveRestoreProgressState(
|
||||
restoreState = SignalStore.backup.restoreState,
|
||||
remainingRestoreSize = SignalStore.backup.totalRestorableAttachmentSize.bytes,
|
||||
totalRestoreSize = SignalStore.backup.totalRestorableAttachmentSize.bytes,
|
||||
hasActivelyRestoredThisRun = SignalStore.backup.totalRestorableAttachmentSize > 0,
|
||||
totalToRestoreThisRun = SignalStore.backup.totalRestorableAttachmentSize.bytes,
|
||||
restoreStatus = ArchiveRestoreProgressState.RestoreStatus.NONE
|
||||
)
|
||||
)
|
||||
|
||||
val state: ArchiveRestoreProgressState
|
||||
get() = store.value
|
||||
|
||||
val stateFlow: Flow<ArchiveRestoreProgressState> = store
|
||||
.throttleLatest(1.seconds)
|
||||
.distinctUntilChanged()
|
||||
.flowOn(Dispatchers.IO)
|
||||
|
||||
init {
|
||||
SignalExecutors.BOUNDED.execute { update() }
|
||||
}
|
||||
|
||||
fun onRestorePending() {
|
||||
Log.i(TAG, "onRestorePending")
|
||||
SignalStore.backup.restoreState = RestoreState.PENDING
|
||||
update()
|
||||
}
|
||||
|
||||
fun onStartMediaRestore() {
|
||||
Log.i(TAG, "onStartMediaRestore")
|
||||
SignalStore.backup.restoreState = RestoreState.CALCULATING_MEDIA
|
||||
SignalStore.backup.totalRestorableAttachmentSize = SignalDatabase.attachments.getRemainingRestorableAttachmentSize()
|
||||
update()
|
||||
}
|
||||
|
||||
fun onRestoringMedia() {
|
||||
Log.i(TAG, "onRestoringMedia")
|
||||
SignalStore.backup.restoreState = RestoreState.RESTORING_MEDIA
|
||||
SignalStore.backup.totalRestorableAttachmentSize = SignalDatabase.attachments.getRemainingRestorableAttachmentSize()
|
||||
update()
|
||||
}
|
||||
|
||||
fun onRestoringDb() {
|
||||
Log.i(TAG, "onRestoringDb")
|
||||
SignalStore.backup.restoreState = RestoreState.RESTORING_DB
|
||||
update()
|
||||
}
|
||||
|
||||
fun onCancelMediaRestore() {
|
||||
Log.i(TAG, "onCancelMediaRestore")
|
||||
SignalStore.backup.restoreState = RestoreState.CANCELING_MEDIA
|
||||
update()
|
||||
}
|
||||
|
||||
fun allMediaRestored() {
|
||||
val shouldUpdate = if (SignalStore.backup.restoreState == RestoreState.CANCELING_MEDIA) {
|
||||
Log.i(TAG, "allMediaCanceled")
|
||||
store.update { state ->
|
||||
if (state.restoreState == RestoreState.CANCELING_MEDIA) {
|
||||
state.copy(
|
||||
hasActivelyRestoredThisRun = false,
|
||||
totalToRestoreThisRun = 0.bytes
|
||||
)
|
||||
} else {
|
||||
state
|
||||
}
|
||||
}
|
||||
true
|
||||
} else if (SignalStore.backup.restoreState != RestoreState.NONE) {
|
||||
Log.i(TAG, "allMediaRestored")
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
if (shouldUpdate) {
|
||||
SignalStore.backup.totalRestorableAttachmentSize = 0
|
||||
SignalStore.backup.restoreState = RestoreState.NONE
|
||||
update()
|
||||
onProcessEnd()
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun forceUpdate() {
|
||||
update()
|
||||
}
|
||||
|
||||
fun clearFinishedStatus() {
|
||||
store.update { state ->
|
||||
if (state.restoreStatus == ArchiveRestoreProgressState.RestoreStatus.FINISHED) {
|
||||
state.copy(
|
||||
restoreStatus = ArchiveRestoreProgressState.RestoreStatus.NONE,
|
||||
hasActivelyRestoredThisRun = false,
|
||||
totalToRestoreThisRun = 0.bytes
|
||||
)
|
||||
} else {
|
||||
state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun update() {
|
||||
store.update { state ->
|
||||
val remainingRestoreSize = SignalDatabase.attachments.getRemainingRestorableAttachmentSize().bytes
|
||||
var restoreState = SignalStore.backup.restoreState
|
||||
|
||||
val status = when {
|
||||
!WifiConstraint.isMet(AppDependencies.application) && !SignalStore.backup.restoreWithCellular -> ArchiveRestoreProgressState.RestoreStatus.WAITING_FOR_WIFI
|
||||
!NetworkConstraint.isMet(AppDependencies.application) -> ArchiveRestoreProgressState.RestoreStatus.WAITING_FOR_INTERNET
|
||||
!BatteryNotLowConstraint.isMet() -> ArchiveRestoreProgressState.RestoreStatus.LOW_BATTERY
|
||||
restoreState == RestoreState.NONE -> if (state.hasActivelyRestoredThisRun) ArchiveRestoreProgressState.RestoreStatus.FINISHED else ArchiveRestoreProgressState.RestoreStatus.NONE
|
||||
else -> {
|
||||
val availableBytes = SignalStore.backup.spaceAvailableOnDiskBytes
|
||||
|
||||
if (availableBytes > -1L && remainingRestoreSize > availableBytes.bytes) {
|
||||
ArchiveRestoreProgressState.RestoreStatus.NOT_ENOUGH_DISK_SPACE
|
||||
} else {
|
||||
ArchiveRestoreProgressState.RestoreStatus.RESTORING
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (restoreState.isMediaRestoreOperation) {
|
||||
if (remainingRestoreSize == 0.bytes && SignalStore.backup.totalRestorableAttachmentSize == 0L) {
|
||||
restoreState = RestoreState.NONE
|
||||
SignalStore.backup.restoreState = restoreState
|
||||
} else {
|
||||
registerUpdateListeners()
|
||||
}
|
||||
} else {
|
||||
unregisterUpdateListeners()
|
||||
}
|
||||
|
||||
val totalRestoreSize = SignalStore.backup.totalRestorableAttachmentSize.bytes
|
||||
|
||||
state.copy(
|
||||
restoreState = restoreState,
|
||||
remainingRestoreSize = remainingRestoreSize,
|
||||
restoreStatus = status,
|
||||
totalRestoreSize = totalRestoreSize,
|
||||
hasActivelyRestoredThisRun = state.hasActivelyRestoredThisRun || SignalStore.backup.totalRestorableAttachmentSize > 0,
|
||||
totalToRestoreThisRun = if (totalRestoreSize > 0.bytes) totalRestoreSize else state.totalToRestoreThisRun
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun registerUpdateListeners() {
|
||||
if (!listenersRegistered) {
|
||||
listenerLock.withLock {
|
||||
if (!listenersRegistered) {
|
||||
Log.i(TAG, "Registering progress related listeners")
|
||||
AppDependencies.databaseObserver.registerAttachmentUpdatedObserver(attachmentObserver)
|
||||
AppDependencies.application.registerReceiver(networkChangeReceiver, IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION))
|
||||
AppDependencies.application.registerReceiver(networkChangeReceiver, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
|
||||
listenersRegistered = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun unregisterUpdateListeners() {
|
||||
if (listenersRegistered) {
|
||||
listenerLock.withLock {
|
||||
if (listenersRegistered) {
|
||||
Log.i(TAG, "Unregistering listeners")
|
||||
AppDependencies.databaseObserver.unregisterObserver(attachmentObserver)
|
||||
AppDependencies.application.safeUnregisterReceiver(networkChangeReceiver)
|
||||
listenersRegistered = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//region Attachment Debug
|
||||
|
||||
private var debugAttachmentStartTime: Long = 0
|
||||
private val debugTotalAttachments: AtomicInteger = AtomicInteger(0)
|
||||
private val debugTotalBytes: AtomicLong = AtomicLong(0)
|
||||
@@ -53,7 +270,7 @@ object ArchiveRestoreProgress {
|
||||
}
|
||||
}
|
||||
|
||||
fun onProcessEnd() {
|
||||
private fun onProcessEnd() {
|
||||
if (debugAttachmentStartTime <= 0 || debugTotalAttachments.get() <= 0 || debugTotalBytes.get() <= 0) {
|
||||
Log.w(TAG, "Insufficient data to print debug stats.")
|
||||
return
|
||||
@@ -84,4 +301,6 @@ object ArchiveRestoreProgress {
|
||||
return "Duration=${System.currentTimeMillis() - startTimeMs}ms, TotalBytes=$totalBytes (${totalBytes.bytes.toUnitString()}), NetworkRate=$networkBytesPerSecond bytes/sec (${networkBytesPerSecond.bytes.toUnitString()}/sec), DiskRate=$diskBytesPerSecond bytes/sec (${diskBytesPerSecond.bytes.toUnitString()}/sec)"
|
||||
}
|
||||
}
|
||||
|
||||
//endregion Attachment Debug
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2
|
||||
|
||||
import org.signal.core.util.ByteSize
|
||||
import org.signal.core.util.bytes
|
||||
import org.thoughtcrime.securesms.backup.RestoreState
|
||||
|
||||
/**
|
||||
* In-memory view of the current state of an attachment restore process.
|
||||
*/
|
||||
data class ArchiveRestoreProgressState(
|
||||
val restoreState: RestoreState,
|
||||
val remainingRestoreSize: ByteSize,
|
||||
val totalRestoreSize: ByteSize,
|
||||
val hasActivelyRestoredThisRun: Boolean = false,
|
||||
val totalToRestoreThisRun: ByteSize = 0.bytes,
|
||||
val restoreStatus: RestoreStatus
|
||||
) {
|
||||
val completedRestoredSize = totalRestoreSize - remainingRestoreSize
|
||||
|
||||
val progress: Float? = when (this.restoreState) {
|
||||
RestoreState.CALCULATING_MEDIA,
|
||||
RestoreState.CANCELING_MEDIA -> this.completedRestoredSize.percentageOf(this.totalRestoreSize)
|
||||
|
||||
RestoreState.RESTORING_MEDIA -> {
|
||||
when (this.restoreStatus) {
|
||||
RestoreStatus.NONE -> null
|
||||
RestoreStatus.FINISHED -> 1f
|
||||
else -> this.completedRestoredSize.percentageOf(this.totalRestoreSize)
|
||||
}
|
||||
}
|
||||
|
||||
RestoreState.NONE -> {
|
||||
if (this.restoreStatus == RestoreStatus.FINISHED) {
|
||||
1f
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
else -> null
|
||||
}
|
||||
|
||||
fun activelyRestoring(): Boolean {
|
||||
return restoreState.inProgress
|
||||
}
|
||||
|
||||
fun needRestoreMediaService(): Boolean {
|
||||
return (restoreState == RestoreState.CALCULATING_MEDIA || restoreState == RestoreState.RESTORING_MEDIA) &&
|
||||
totalRestoreSize > 0.bytes &&
|
||||
remainingRestoreSize != 0.bytes
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes the status of an in-progress media download session.
|
||||
*/
|
||||
enum class RestoreStatus {
|
||||
NONE,
|
||||
RESTORING,
|
||||
LOW_BATTERY,
|
||||
WAITING_FOR_INTERNET,
|
||||
WAITING_FOR_WIFI,
|
||||
NOT_ENOUGH_DISK_SPACE,
|
||||
FINISHED
|
||||
}
|
||||
}
|
||||
@@ -59,7 +59,6 @@ import org.thoughtcrime.securesms.attachments.Cdn
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
|
||||
import org.thoughtcrime.securesms.backup.ArchiveUploadProgress
|
||||
import org.thoughtcrime.securesms.backup.DeletionState
|
||||
import org.thoughtcrime.securesms.backup.RestoreState
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository.copyAttachmentToArchive
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository.exportForDebugging
|
||||
import org.thoughtcrime.securesms.backup.v2.importer.ChatItemArchiveImporter
|
||||
@@ -105,11 +104,10 @@ import org.thoughtcrime.securesms.jobs.AvatarGroupsV2DownloadJob
|
||||
import org.thoughtcrime.securesms.jobs.BackupDeleteJob
|
||||
import org.thoughtcrime.securesms.jobs.BackupMessagesJob
|
||||
import org.thoughtcrime.securesms.jobs.BackupRestoreMediaJob
|
||||
import org.thoughtcrime.securesms.jobs.CheckRestoreMediaLeftJob
|
||||
import org.thoughtcrime.securesms.jobs.CancelRestoreMediaJob
|
||||
import org.thoughtcrime.securesms.jobs.CreateReleaseChannelJob
|
||||
import org.thoughtcrime.securesms.jobs.LocalBackupJob
|
||||
import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob
|
||||
import org.thoughtcrime.securesms.jobs.RestoreAttachmentJob
|
||||
import org.thoughtcrime.securesms.jobs.RestoreOptimizedMediaJob
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob
|
||||
import org.thoughtcrime.securesms.jobs.StickerPackDownloadJob
|
||||
@@ -362,11 +360,7 @@ object BackupRepository {
|
||||
*/
|
||||
@JvmStatic
|
||||
fun skipMediaRestore() {
|
||||
SignalStore.backup.userManuallySkippedMediaRestore = true
|
||||
|
||||
RestoreAttachmentJob.Queues.ALL.forEach { AppDependencies.jobManager.cancelAllInQueue(it) }
|
||||
|
||||
RestoreAttachmentJob.Queues.ALL.forEach { AppDependencies.jobManager.add(CheckRestoreMediaLeftJob(it)) }
|
||||
CancelRestoreMediaJob.enqueue()
|
||||
}
|
||||
|
||||
fun markBackupFailure() {
|
||||
@@ -1991,7 +1985,7 @@ object BackupRepository {
|
||||
|
||||
suspend fun restoreRemoteBackup(): RemoteRestoreResult {
|
||||
val context = AppDependencies.application
|
||||
SignalStore.backup.restoreState = RestoreState.PENDING
|
||||
ArchiveRestoreProgress.onRestorePending()
|
||||
|
||||
try {
|
||||
DataRestoreConstraint.isRestoringData = true
|
||||
@@ -2006,7 +2000,7 @@ object BackupRepository {
|
||||
}
|
||||
|
||||
private fun restoreRemoteBackup(controller: BackupProgressService.Controller, cancellationSignal: () -> Boolean): RemoteRestoreResult {
|
||||
SignalStore.backup.restoreState = RestoreState.RESTORING_DB
|
||||
ArchiveRestoreProgress.onRestoringDb()
|
||||
|
||||
val progressListener = object : ProgressListener {
|
||||
override fun onAttachmentProgress(progress: AttachmentTransferProgress) {
|
||||
@@ -2098,8 +2092,6 @@ object BackupRepository {
|
||||
return RemoteRestoreResult.Failure
|
||||
}
|
||||
|
||||
SignalStore.backup.restoreState = RestoreState.RESTORING_MEDIA
|
||||
|
||||
BackupMediaRestoreService.resetTimeout()
|
||||
AppDependencies.jobManager.add(BackupRestoreMediaJob())
|
||||
|
||||
@@ -2109,7 +2101,7 @@ object BackupRepository {
|
||||
|
||||
suspend fun restoreLinkAndSyncBackup(response: TransferArchiveResponse, ephemeralBackupKey: MessageBackupKey) {
|
||||
val context = AppDependencies.application
|
||||
SignalStore.backup.restoreState = RestoreState.PENDING
|
||||
ArchiveRestoreProgress.onRestorePending()
|
||||
|
||||
try {
|
||||
DataRestoreConstraint.isRestoringData = true
|
||||
@@ -2124,7 +2116,7 @@ object BackupRepository {
|
||||
}
|
||||
|
||||
private fun restoreLinkAndSyncBackup(response: TransferArchiveResponse, ephemeralBackupKey: MessageBackupKey, controller: BackupProgressService.Controller, cancellationSignal: () -> Boolean): RemoteRestoreResult {
|
||||
SignalStore.backup.restoreState = RestoreState.RESTORING_DB
|
||||
ArchiveRestoreProgress.onRestoringDb()
|
||||
|
||||
val progressListener = object : ProgressListener {
|
||||
override fun onAttachmentProgress(progress: AttachmentTransferProgress) {
|
||||
@@ -2175,8 +2167,6 @@ object BackupRepository {
|
||||
return RemoteRestoreResult.Failure
|
||||
}
|
||||
|
||||
SignalStore.backup.restoreState = RestoreState.RESTORING_MEDIA
|
||||
|
||||
BackupMediaRestoreService.resetTimeout()
|
||||
AppDependencies.jobManager.add(BackupRestoreMediaJob())
|
||||
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2.ui.status
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.text.InlineTextContent
|
||||
import androidx.compose.foundation.text.appendInlineContent
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.LinkAnnotation
|
||||
import androidx.compose.ui.text.Placeholder
|
||||
import androidx.compose.ui.text.PlaceholderVerticalAlign
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.TextLinkStyles
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.withLink
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.SignalPreview
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.signal.core.ui.R as CoreUiR
|
||||
|
||||
private val YELLOW_DOT = Color(0xFFFFCC00)
|
||||
|
||||
/**
|
||||
* Show backup creation failures as a settings row.
|
||||
*/
|
||||
@Composable
|
||||
fun BackupCreateErrorRow(
|
||||
showCouldNotComplete: Boolean,
|
||||
showBackupFailed: Boolean,
|
||||
onLearnMoreClick: () -> Unit = {}
|
||||
) {
|
||||
if (showBackupFailed) {
|
||||
val inlineContentMap = mapOf(
|
||||
"yellow_bullet" to InlineTextContent(
|
||||
Placeholder(20.sp, 12.sp, PlaceholderVerticalAlign.TextCenter)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(12.dp)
|
||||
.background(color = YELLOW_DOT, shape = CircleShape)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
BackupAlertText(
|
||||
text = buildAnnotatedString {
|
||||
appendInlineContent("yellow_bullet")
|
||||
append(" ")
|
||||
append(stringResource(R.string.BackupStatusRow__your_last_backup_latest_version))
|
||||
append(" ")
|
||||
withLink(
|
||||
LinkAnnotation.Clickable(
|
||||
stringResource(R.string.BackupStatusRow__learn_more),
|
||||
styles = TextLinkStyles(style = SpanStyle(color = MaterialTheme.colorScheme.primary))
|
||||
) {
|
||||
onLearnMoreClick()
|
||||
}
|
||||
) {
|
||||
append(stringResource(R.string.BackupStatusRow__learn_more))
|
||||
}
|
||||
},
|
||||
inlineContent = inlineContentMap
|
||||
)
|
||||
} else if (showCouldNotComplete) {
|
||||
val inlineContentMap = mapOf(
|
||||
"yellow_bullet" to InlineTextContent(
|
||||
Placeholder(20.sp, 12.sp, PlaceholderVerticalAlign.TextCenter)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(12.dp)
|
||||
.background(color = YELLOW_DOT, shape = CircleShape)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
BackupAlertText(
|
||||
text = buildAnnotatedString {
|
||||
appendInlineContent("yellow_bullet")
|
||||
append(" ")
|
||||
append(stringResource(R.string.BackupStatusRow__your_last_backup))
|
||||
},
|
||||
inlineContent = inlineContentMap
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BackupAlertText(text: AnnotatedString, inlineContent: Map<String, InlineTextContent>) {
|
||||
Text(
|
||||
text = text,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(horizontal = dimensionResource(CoreUiR.dimen.gutter)),
|
||||
inlineContent = inlineContent
|
||||
)
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
fun BackupStatusRowCouldNotCompleteBackupPreview() {
|
||||
Previews.Preview {
|
||||
BackupCreateErrorRow(showCouldNotComplete = true, showBackupFailed = false)
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
fun BackupStatusRowBackupFailedPreview() {
|
||||
Previews.Preview {
|
||||
BackupCreateErrorRow(showCouldNotComplete = false, showBackupFailed = true)
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,6 @@
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2.ui.status
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
@@ -31,6 +29,7 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -38,14 +37,12 @@ import androidx.compose.ui.unit.dp
|
||||
import org.signal.core.ui.compose.Buttons
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.SignalPreview
|
||||
import org.signal.core.util.ByteSize
|
||||
import org.signal.core.util.bytes
|
||||
import org.signal.core.util.kibiBytes
|
||||
import org.signal.core.util.mebiBytes
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.backup.RestoreState
|
||||
import org.thoughtcrime.securesms.backup.v2.ArchiveRestoreProgressState
|
||||
import org.thoughtcrime.securesms.backup.v2.ArchiveRestoreProgressState.RestoreStatus
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.BackupsIconColors
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
private const val NONE = -1
|
||||
|
||||
@@ -56,12 +53,16 @@ private const val NONE = -1
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun BackupStatusBanner(
|
||||
data: BackupStatusData,
|
||||
data: ArchiveRestoreProgressState,
|
||||
onBannerClick: () -> Unit = {},
|
||||
onActionClick: (BackupStatusData) -> Unit = {},
|
||||
onActionClick: (ArchiveRestoreProgressState) -> Unit = {},
|
||||
onDismissClick: () -> Unit = {},
|
||||
contentPadding: PaddingValues = PaddingValues(horizontal = 12.dp, vertical = 8.dp)
|
||||
) {
|
||||
if (!data.restoreState.isMediaRestoreOperation && data.restoreStatus != RestoreStatus.FINISHED) {
|
||||
return
|
||||
}
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
@@ -73,9 +74,9 @@ fun BackupStatusBanner(
|
||||
.padding(12.dp)
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = data.iconRes),
|
||||
painter = painterResource(id = data.iconResource()),
|
||||
contentDescription = null,
|
||||
tint = data.iconColors.foreground,
|
||||
tint = data.iconColor(),
|
||||
modifier = Modifier
|
||||
.padding(start = 4.dp)
|
||||
.size(24.dp)
|
||||
@@ -89,7 +90,7 @@ fun BackupStatusBanner(
|
||||
.weight(1f)
|
||||
) {
|
||||
Text(
|
||||
text = data.title,
|
||||
text = data.title(),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier
|
||||
@@ -97,7 +98,7 @@ fun BackupStatusBanner(
|
||||
.align(Alignment.CenterVertically)
|
||||
)
|
||||
|
||||
data.status?.let { status ->
|
||||
data.status()?.let { status ->
|
||||
Text(
|
||||
text = status,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
@@ -109,9 +110,12 @@ fun BackupStatusBanner(
|
||||
}
|
||||
}
|
||||
|
||||
if (data.progress >= 0f) {
|
||||
if (data.restoreState == RestoreState.CALCULATING_MEDIA ||
|
||||
data.restoreState == RestoreState.CANCELING_MEDIA ||
|
||||
(data.restoreState == RestoreState.RESTORING_MEDIA && data.restoreStatus == RestoreStatus.RESTORING)
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
progress = { data.progress },
|
||||
progress = { data.progress!! },
|
||||
strokeWidth = 3.dp,
|
||||
strokeCap = StrokeCap.Round,
|
||||
modifier = Modifier
|
||||
@@ -119,16 +123,16 @@ fun BackupStatusBanner(
|
||||
)
|
||||
}
|
||||
|
||||
if (data.actionRes != NONE) {
|
||||
if (data.actionResource() != NONE) {
|
||||
Buttons.Small(
|
||||
onClick = { onActionClick(data) },
|
||||
modifier = Modifier.padding(start = 8.dp)
|
||||
) {
|
||||
Text(text = stringResource(id = data.actionRes))
|
||||
Text(text = stringResource(id = data.actionResource()))
|
||||
}
|
||||
}
|
||||
|
||||
if (data.showDismissAction) {
|
||||
if (data.restoreStatus == RestoreStatus.FINISHED) {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
|
||||
Icon(
|
||||
@@ -147,195 +151,208 @@ fun BackupStatusBanner(
|
||||
}
|
||||
}
|
||||
|
||||
private fun ArchiveRestoreProgressState.iconResource(): Int {
|
||||
return when (this.restoreState) {
|
||||
RestoreState.CALCULATING_MEDIA,
|
||||
RestoreState.CANCELING_MEDIA -> R.drawable.symbol_backup_light
|
||||
|
||||
RestoreState.RESTORING_MEDIA -> {
|
||||
when (this.restoreStatus) {
|
||||
RestoreStatus.RESTORING,
|
||||
RestoreStatus.WAITING_FOR_INTERNET,
|
||||
RestoreStatus.WAITING_FOR_WIFI,
|
||||
RestoreStatus.LOW_BATTERY -> R.drawable.symbol_backup_light
|
||||
|
||||
RestoreStatus.NOT_ENOUGH_DISK_SPACE -> R.drawable.symbol_backup_error_24
|
||||
RestoreStatus.FINISHED -> R.drawable.symbol_check_circle_24
|
||||
RestoreStatus.NONE -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
|
||||
RestoreState.NONE -> {
|
||||
if (this.restoreStatus == RestoreStatus.FINISHED) {
|
||||
R.drawable.symbol_check_circle_24
|
||||
} else {
|
||||
throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
|
||||
RestoreState.PENDING,
|
||||
RestoreState.RESTORING_DB -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ArchiveRestoreProgressState.iconColor(): Color {
|
||||
return when (this.restoreState) {
|
||||
RestoreState.CALCULATING_MEDIA,
|
||||
RestoreState.CANCELING_MEDIA -> BackupsIconColors.Normal.foreground
|
||||
|
||||
RestoreState.RESTORING_MEDIA -> {
|
||||
when (this.restoreStatus) {
|
||||
RestoreStatus.RESTORING -> BackupsIconColors.Normal.foreground
|
||||
RestoreStatus.WAITING_FOR_INTERNET,
|
||||
RestoreStatus.WAITING_FOR_WIFI,
|
||||
RestoreStatus.LOW_BATTERY,
|
||||
RestoreStatus.NOT_ENOUGH_DISK_SPACE -> BackupsIconColors.Warning.foreground
|
||||
|
||||
RestoreStatus.FINISHED -> BackupsIconColors.Success.foreground
|
||||
RestoreStatus.NONE -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
|
||||
RestoreState.NONE -> {
|
||||
if (this.restoreStatus == RestoreStatus.FINISHED) {
|
||||
BackupsIconColors.Success.foreground
|
||||
} else {
|
||||
throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
|
||||
RestoreState.PENDING,
|
||||
RestoreState.RESTORING_DB -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ArchiveRestoreProgressState.title(): String {
|
||||
return when (this.restoreState) {
|
||||
RestoreState.CALCULATING_MEDIA -> stringResource(R.string.BackupStatus__restoring_media)
|
||||
RestoreState.CANCELING_MEDIA -> stringResource(R.string.BackupStatus__cancel_restore_media)
|
||||
RestoreState.RESTORING_MEDIA -> {
|
||||
when (this.restoreStatus) {
|
||||
RestoreStatus.RESTORING -> stringResource(R.string.BackupStatus__restoring_media)
|
||||
RestoreStatus.WAITING_FOR_INTERNET,
|
||||
RestoreStatus.WAITING_FOR_WIFI,
|
||||
RestoreStatus.LOW_BATTERY -> stringResource(R.string.BackupStatus__restore_paused)
|
||||
|
||||
RestoreStatus.NOT_ENOUGH_DISK_SPACE -> {
|
||||
stringResource(R.string.BackupStatus__free_up_s_of_space_to_download_your_media, this.remainingRestoreSize.toUnitString())
|
||||
}
|
||||
|
||||
RestoreStatus.FINISHED -> stringResource(R.string.BackupStatus__restore_complete)
|
||||
RestoreStatus.NONE -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
|
||||
RestoreState.NONE -> {
|
||||
if (this.restoreStatus == RestoreStatus.FINISHED) {
|
||||
stringResource(R.string.BackupStatus__restore_complete)
|
||||
} else {
|
||||
throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
|
||||
RestoreState.PENDING,
|
||||
RestoreState.RESTORING_DB -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ArchiveRestoreProgressState.status(): String? {
|
||||
return when (this.restoreState) {
|
||||
RestoreState.CALCULATING_MEDIA -> {
|
||||
stringResource(
|
||||
R.string.BackupStatus__status_size_of_size,
|
||||
this.completedRestoredSize.toUnitString(),
|
||||
this.totalRestoreSize.toUnitString()
|
||||
)
|
||||
}
|
||||
|
||||
RestoreState.CANCELING_MEDIA -> null
|
||||
RestoreState.RESTORING_MEDIA -> {
|
||||
when (this.restoreStatus) {
|
||||
RestoreStatus.RESTORING -> {
|
||||
stringResource(
|
||||
R.string.BackupStatus__status_size_of_size,
|
||||
this.completedRestoredSize.toUnitString(),
|
||||
this.totalRestoreSize.toUnitString()
|
||||
)
|
||||
}
|
||||
|
||||
RestoreStatus.WAITING_FOR_INTERNET -> stringResource(R.string.BackupStatus__status_no_internet)
|
||||
RestoreStatus.WAITING_FOR_WIFI -> stringResource(R.string.BackupStatus__status_waiting_for_wifi)
|
||||
RestoreStatus.LOW_BATTERY -> stringResource(R.string.BackupStatus__status_device_has_low_battery)
|
||||
RestoreStatus.NOT_ENOUGH_DISK_SPACE -> null
|
||||
RestoreStatus.FINISHED -> this.totalToRestoreThisRun.toUnitString()
|
||||
RestoreStatus.NONE -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
|
||||
RestoreState.NONE -> {
|
||||
if (this.restoreStatus == RestoreStatus.FINISHED) {
|
||||
this.totalToRestoreThisRun.toUnitString()
|
||||
} else {
|
||||
throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
|
||||
RestoreState.PENDING,
|
||||
RestoreState.RESTORING_DB -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
|
||||
private fun ArchiveRestoreProgressState.actionResource(): Int {
|
||||
return when (this.restoreState) {
|
||||
RestoreState.CALCULATING_MEDIA,
|
||||
RestoreState.CANCELING_MEDIA -> NONE
|
||||
|
||||
RestoreState.RESTORING_MEDIA -> {
|
||||
when (this.restoreStatus) {
|
||||
RestoreStatus.NOT_ENOUGH_DISK_SPACE -> R.string.BackupStatus__details
|
||||
RestoreStatus.WAITING_FOR_WIFI -> R.string.BackupStatus__resume
|
||||
else -> NONE
|
||||
}
|
||||
}
|
||||
|
||||
else -> NONE
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
fun BackupStatusBannerPreview() {
|
||||
Previews.Preview {
|
||||
Column {
|
||||
BackupStatusBanner(
|
||||
data = BackupStatusData.RestoringMedia(5755000.bytes, 1253.mebiBytes)
|
||||
data = ArchiveRestoreProgressState(restoreState = RestoreState.RESTORING_MEDIA, restoreStatus = RestoreStatus.RESTORING, remainingRestoreSize = 800.mebiBytes, totalRestoreSize = 1024.mebiBytes)
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
BackupStatusBanner(
|
||||
data = BackupStatusData.RestoringMedia(
|
||||
bytesDownloaded = 55000.bytes,
|
||||
bytesTotal = 1253.mebiBytes,
|
||||
restoreStatus = BackupStatusData.RestoreStatus.WAITING_FOR_WIFI
|
||||
)
|
||||
data = ArchiveRestoreProgressState(restoreState = RestoreState.CALCULATING_MEDIA, restoreStatus = RestoreStatus.RESTORING, remainingRestoreSize = 1024.mebiBytes, totalRestoreSize = 1024.mebiBytes)
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
BackupStatusBanner(
|
||||
data = BackupStatusData.RestoringMedia(
|
||||
bytesDownloaded = 55000.bytes,
|
||||
bytesTotal = 1253.mebiBytes,
|
||||
restoreStatus = BackupStatusData.RestoreStatus.WAITING_FOR_INTERNET
|
||||
)
|
||||
data = ArchiveRestoreProgressState(restoreState = RestoreState.CANCELING_MEDIA, restoreStatus = RestoreStatus.RESTORING, remainingRestoreSize = 200.mebiBytes, totalRestoreSize = 1024.mebiBytes)
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
BackupStatusBanner(
|
||||
data = BackupStatusData.RestoringMedia(
|
||||
bytesDownloaded = 55000.bytes,
|
||||
bytesTotal = 1253.mebiBytes,
|
||||
restoreStatus = BackupStatusData.RestoreStatus.FINISHED
|
||||
)
|
||||
data = ArchiveRestoreProgressState(restoreState = RestoreState.RESTORING_MEDIA, restoreStatus = RestoreStatus.WAITING_FOR_WIFI, remainingRestoreSize = 800.mebiBytes, totalRestoreSize = 1024.mebiBytes)
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
BackupStatusBanner(
|
||||
data = BackupStatusData.NotEnoughFreeSpace(40900.kibiBytes)
|
||||
data = ArchiveRestoreProgressState(restoreState = RestoreState.RESTORING_MEDIA, restoreStatus = RestoreStatus.WAITING_FOR_INTERNET, remainingRestoreSize = 800.mebiBytes, totalRestoreSize = 1024.mebiBytes)
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
BackupStatusBanner(
|
||||
data = BackupStatusData.CouldNotCompleteBackup
|
||||
data = ArchiveRestoreProgressState(restoreState = RestoreState.NONE, restoreStatus = RestoreStatus.FINISHED, remainingRestoreSize = 0.mebiBytes, totalRestoreSize = 0.mebiBytes, totalToRestoreThisRun = 1024.mebiBytes)
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
BackupStatusBanner(
|
||||
data = BackupStatusData.BackupFailed
|
||||
data = ArchiveRestoreProgressState(restoreState = RestoreState.RESTORING_MEDIA, restoreStatus = RestoreStatus.NOT_ENOUGH_DISK_SPACE, remainingRestoreSize = 500.mebiBytes, totalRestoreSize = 1024.mebiBytes)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sealed interface describing status data to display in BackupStatus widget.
|
||||
*/
|
||||
sealed interface BackupStatusData {
|
||||
|
||||
@get:DrawableRes
|
||||
val iconRes: Int
|
||||
|
||||
@get:Composable
|
||||
val title: String
|
||||
|
||||
val iconColors: BackupsIconColors
|
||||
|
||||
@get:StringRes
|
||||
val actionRes: Int get() = NONE
|
||||
|
||||
@get:Composable
|
||||
val status: String? get() = null
|
||||
|
||||
val progress: Float get() = NONE.toFloat()
|
||||
|
||||
val showDismissAction: Boolean get() = false
|
||||
|
||||
/**
|
||||
* Generic failure
|
||||
*/
|
||||
data object CouldNotCompleteBackup : BackupStatusData {
|
||||
override val iconRes: Int = R.drawable.symbol_backup_error_24
|
||||
|
||||
override val title: String
|
||||
@Composable
|
||||
get() = stringResource(androidx.biometric.R.string.default_error_msg)
|
||||
|
||||
override val iconColors: BackupsIconColors = BackupsIconColors.Warning
|
||||
}
|
||||
|
||||
/**
|
||||
* Initial backup creation failure
|
||||
*/
|
||||
data object BackupFailed : BackupStatusData {
|
||||
override val iconRes: Int = R.drawable.symbol_backup_error_24
|
||||
|
||||
override val title: String
|
||||
@Composable
|
||||
get() = stringResource(androidx.biometric.R.string.default_error_msg)
|
||||
|
||||
override val iconColors: BackupsIconColors = BackupsIconColors.Warning
|
||||
}
|
||||
|
||||
/**
|
||||
* User does not have enough space on their device to complete backup restoration
|
||||
*/
|
||||
class NotEnoughFreeSpace(
|
||||
requiredSpace: ByteSize
|
||||
) : BackupStatusData {
|
||||
val requiredSpace = requiredSpace.toUnitString()
|
||||
|
||||
override val iconRes: Int = R.drawable.symbol_backup_error_24
|
||||
|
||||
override val title: String
|
||||
@Composable
|
||||
get() = stringResource(R.string.BackupStatus__free_up_s_of_space_to_download_your_media, requiredSpace)
|
||||
|
||||
override val iconColors: BackupsIconColors = BackupsIconColors.Warning
|
||||
override val actionRes: Int = R.string.BackupStatus__details
|
||||
}
|
||||
|
||||
/**
|
||||
* Restoring media, finished, and paused states.
|
||||
*/
|
||||
data class RestoringMedia(
|
||||
val bytesDownloaded: ByteSize = 0.bytes,
|
||||
val bytesTotal: ByteSize = 0.bytes,
|
||||
val restoreStatus: RestoreStatus = RestoreStatus.NORMAL
|
||||
) : BackupStatusData {
|
||||
override val iconRes: Int = if (restoreStatus == RestoreStatus.FINISHED) R.drawable.symbol_check_circle_24 else R.drawable.symbol_backup_light
|
||||
override val iconColors: BackupsIconColors = when (restoreStatus) {
|
||||
RestoreStatus.FINISHED -> BackupsIconColors.Success
|
||||
RestoreStatus.NORMAL -> BackupsIconColors.Normal
|
||||
RestoreStatus.LOW_BATTERY,
|
||||
RestoreStatus.WAITING_FOR_INTERNET,
|
||||
RestoreStatus.WAITING_FOR_WIFI -> BackupsIconColors.Warning
|
||||
}
|
||||
override val showDismissAction: Boolean = restoreStatus == RestoreStatus.FINISHED
|
||||
override val actionRes: Int = when (restoreStatus) {
|
||||
RestoreStatus.WAITING_FOR_WIFI -> R.string.BackupStatus__resume
|
||||
else -> NONE
|
||||
}
|
||||
|
||||
override val title: String
|
||||
@Composable get() = stringResource(
|
||||
when (restoreStatus) {
|
||||
RestoreStatus.NORMAL -> R.string.BackupStatus__restoring_media
|
||||
RestoreStatus.LOW_BATTERY -> R.string.BackupStatus__restore_paused
|
||||
RestoreStatus.WAITING_FOR_INTERNET -> R.string.BackupStatus__restore_paused
|
||||
RestoreStatus.WAITING_FOR_WIFI -> R.string.BackupStatus__restore_paused
|
||||
RestoreStatus.FINISHED -> R.string.BackupStatus__restore_complete
|
||||
}
|
||||
)
|
||||
|
||||
override val status: String
|
||||
@Composable get() = when (restoreStatus) {
|
||||
RestoreStatus.NORMAL -> stringResource(
|
||||
R.string.BackupStatus__status_size_of_size,
|
||||
bytesDownloaded.toUnitString(),
|
||||
bytesTotal.toUnitString()
|
||||
)
|
||||
|
||||
RestoreStatus.LOW_BATTERY -> stringResource(R.string.BackupStatus__status_device_has_low_battery)
|
||||
RestoreStatus.WAITING_FOR_INTERNET -> stringResource(R.string.BackupStatus__status_no_internet)
|
||||
RestoreStatus.WAITING_FOR_WIFI -> stringResource(R.string.BackupStatus__status_waiting_for_wifi)
|
||||
RestoreStatus.FINISHED -> bytesTotal.toUnitString()
|
||||
}
|
||||
|
||||
override val progress: Float = if (bytesTotal.bytes > 0 && restoreStatus == RestoreStatus.NORMAL) {
|
||||
min(1f, max(0f, bytesDownloaded.bytes.toFloat() / bytesTotal.bytes.toFloat()))
|
||||
} else {
|
||||
NONE.toFloat()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes the status of an in-progress media download session.
|
||||
*/
|
||||
enum class RestoreStatus {
|
||||
NORMAL,
|
||||
LOW_BATTERY,
|
||||
WAITING_FOR_INTERNET,
|
||||
WAITING_FOR_WIFI,
|
||||
FINISHED
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,48 +5,34 @@
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2.ui.status
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.text.InlineTextContent
|
||||
import androidx.compose.foundation.text.appendInlineContent
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.LinkAnnotation
|
||||
import androidx.compose.ui.text.Placeholder
|
||||
import androidx.compose.ui.text.PlaceholderVerticalAlign
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.TextLinkStyles
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.withLink
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.Rows
|
||||
import org.signal.core.ui.compose.SignalPreview
|
||||
import org.signal.core.util.ByteSize
|
||||
import org.signal.core.util.mebiBytes
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.backup.RestoreState
|
||||
import org.thoughtcrime.securesms.backup.v2.ArchiveRestoreProgressState
|
||||
import org.thoughtcrime.securesms.backup.v2.ArchiveRestoreProgressState.RestoreStatus
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.BackupsIconColors
|
||||
import kotlin.math.roundToInt
|
||||
import org.signal.core.ui.R as CoreUiR
|
||||
|
||||
private val YELLOW_DOT = Color(0xFFFFCC00)
|
||||
|
||||
/**
|
||||
* Specifies what kind of restore this is. Slightly different messaging
|
||||
* is utilized for downloads.
|
||||
@@ -69,11 +55,10 @@ enum class RestoreType {
|
||||
*/
|
||||
@Composable
|
||||
fun BackupStatusRow(
|
||||
backupStatusData: BackupStatusData,
|
||||
backupStatusData: ArchiveRestoreProgressState,
|
||||
restoreType: RestoreType = RestoreType.RESTORE,
|
||||
onSkipClick: () -> Unit = {},
|
||||
onCancelClick: (() -> Unit)? = null,
|
||||
onLearnMoreClick: () -> Unit = {}
|
||||
onCancelClick: (() -> Unit)? = null
|
||||
) {
|
||||
val endPad = if (onCancelClick == null) {
|
||||
dimensionResource(CoreUiR.dimen.gutter)
|
||||
@@ -84,183 +69,155 @@ fun BackupStatusRow(
|
||||
Column(
|
||||
modifier = Modifier.padding(top = 8.dp, bottom = 12.dp)
|
||||
) {
|
||||
if (backupStatusData !is BackupStatusData.CouldNotCompleteBackup &&
|
||||
backupStatusData !is BackupStatusData.BackupFailed
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(
|
||||
start = dimensionResource(CoreUiR.dimen.gutter),
|
||||
end = endPad
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(
|
||||
start = dimensionResource(CoreUiR.dimen.gutter),
|
||||
end = endPad
|
||||
)
|
||||
) {
|
||||
LinearProgressIndicator(
|
||||
color = progressColor(backupStatusData),
|
||||
progress = { backupStatusData.progress },
|
||||
modifier = Modifier.weight(1f).padding(vertical = 12.dp),
|
||||
gapSize = 0.dp,
|
||||
drawStopIndicator = {}
|
||||
)
|
||||
LinearProgressIndicator(
|
||||
color = progressColor(backupStatusData),
|
||||
progress = { backupStatusData.progress ?: 0f },
|
||||
modifier = Modifier.weight(1f).padding(vertical = 12.dp),
|
||||
gapSize = 0.dp,
|
||||
drawStopIndicator = {}
|
||||
)
|
||||
|
||||
val isFinished = backupStatusData is BackupStatusData.RestoringMedia && backupStatusData.restoreStatus == BackupStatusData.RestoreStatus.FINISHED
|
||||
if (onCancelClick != null && !isFinished) {
|
||||
IconButton(
|
||||
onClick = onCancelClick
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.symbol_x_24),
|
||||
contentDescription = stringResource(R.string.BackupStatusRow__cancel_download)
|
||||
)
|
||||
}
|
||||
if (onCancelClick != null) {
|
||||
IconButton(
|
||||
onClick = onCancelClick
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.symbol_x_24),
|
||||
contentDescription = stringResource(R.string.BackupStatusRow__cancel_download)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
when (backupStatusData) {
|
||||
is BackupStatusData.RestoringMedia -> {
|
||||
val string = when (restoreType) {
|
||||
RestoreType.RESTORE -> getRestoringMediaString(backupStatusData)
|
||||
RestoreType.DOWNLOAD -> getDownloadingMediaString(backupStatusData)
|
||||
}
|
||||
if (backupStatusData.restoreStatus == RestoreStatus.NOT_ENOUGH_DISK_SPACE) {
|
||||
BackupAlertText(
|
||||
text = stringResource(R.string.BackupStatusRow__not_enough_space, backupStatusData.remainingRestoreSize)
|
||||
)
|
||||
|
||||
BackupAlertText(text = string)
|
||||
Rows.TextRow(
|
||||
text = stringResource(R.string.BackupStatusRow__skip_download),
|
||||
onClick = onSkipClick
|
||||
)
|
||||
} else {
|
||||
val string = when (restoreType) {
|
||||
RestoreType.RESTORE -> getRestoringMediaString(backupStatusData)
|
||||
RestoreType.DOWNLOAD -> getDownloadingMediaString(backupStatusData)
|
||||
}
|
||||
|
||||
is BackupStatusData.NotEnoughFreeSpace -> {
|
||||
BackupAlertText(
|
||||
text = stringResource(
|
||||
R.string.BackupStatusRow__not_enough_space,
|
||||
backupStatusData.requiredSpace,
|
||||
"%d".format((backupStatusData.progress * 100).roundToInt())
|
||||
)
|
||||
)
|
||||
|
||||
Rows.TextRow(
|
||||
text = stringResource(R.string.BackupStatusRow__skip_download),
|
||||
onClick = onSkipClick
|
||||
)
|
||||
}
|
||||
|
||||
BackupStatusData.CouldNotCompleteBackup -> {
|
||||
val inlineContentMap = mapOf(
|
||||
"yellow_bullet" to InlineTextContent(
|
||||
Placeholder(20.sp, 12.sp, PlaceholderVerticalAlign.TextCenter)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(12.dp)
|
||||
.background(color = YELLOW_DOT, shape = CircleShape)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
BackupAlertText(
|
||||
text = buildAnnotatedString {
|
||||
appendInlineContent("yellow_bullet")
|
||||
append(" ")
|
||||
append(stringResource(R.string.BackupStatusRow__your_last_backup))
|
||||
},
|
||||
inlineContent = inlineContentMap
|
||||
)
|
||||
}
|
||||
BackupStatusData.BackupFailed -> {
|
||||
val inlineContentMap = mapOf(
|
||||
"yellow_bullet" to InlineTextContent(
|
||||
Placeholder(20.sp, 12.sp, PlaceholderVerticalAlign.TextCenter)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(12.dp)
|
||||
.background(color = YELLOW_DOT, shape = CircleShape)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
BackupAlertText(
|
||||
text = buildAnnotatedString {
|
||||
appendInlineContent("yellow_bullet")
|
||||
append(" ")
|
||||
append(stringResource(R.string.BackupStatusRow__your_last_backup_latest_version))
|
||||
append(" ")
|
||||
withLink(
|
||||
LinkAnnotation.Clickable(
|
||||
stringResource(R.string.BackupStatusRow__learn_more),
|
||||
styles = TextLinkStyles(style = SpanStyle(color = MaterialTheme.colorScheme.primary))
|
||||
) {
|
||||
onLearnMoreClick()
|
||||
}
|
||||
) {
|
||||
append(stringResource(R.string.BackupStatusRow__learn_more))
|
||||
}
|
||||
},
|
||||
inlineContent = inlineContentMap
|
||||
)
|
||||
}
|
||||
BackupAlertText(text = string)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BackupAlertText(text: String) {
|
||||
BackupAlertText(
|
||||
text = remember(text) { AnnotatedString(text) },
|
||||
inlineContent = emptyMap()
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BackupAlertText(text: AnnotatedString, inlineContent: Map<String, InlineTextContent>) {
|
||||
Text(
|
||||
text = text,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(horizontal = dimensionResource(CoreUiR.dimen.gutter)),
|
||||
inlineContent = inlineContent
|
||||
modifier = Modifier.padding(horizontal = dimensionResource(CoreUiR.dimen.gutter))
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getRestoringMediaString(backupStatusData: BackupStatusData.RestoringMedia): String {
|
||||
return when (backupStatusData.restoreStatus) {
|
||||
BackupStatusData.RestoreStatus.NORMAL -> {
|
||||
private fun getRestoringMediaString(backupStatusData: ArchiveRestoreProgressState): String {
|
||||
return when (backupStatusData.restoreState) {
|
||||
RestoreState.CALCULATING_MEDIA -> {
|
||||
stringResource(
|
||||
R.string.BackupStatusRow__restoring_s_of_s_s,
|
||||
backupStatusData.bytesDownloaded.toUnitString(2),
|
||||
backupStatusData.bytesTotal.toUnitString(2),
|
||||
"%d".format((backupStatusData.progress * 100).roundToInt())
|
||||
backupStatusData.completedRestoredSize.toUnitString(2),
|
||||
backupStatusData.totalRestoreSize.toUnitString(2),
|
||||
"%d".format(((backupStatusData.progress ?: 0f) * 100).roundToInt())
|
||||
)
|
||||
}
|
||||
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)
|
||||
RestoreState.CANCELING_MEDIA -> stringResource(R.string.BackupStatus__cancel_restore_media)
|
||||
RestoreState.RESTORING_MEDIA -> {
|
||||
when (backupStatusData.restoreStatus) {
|
||||
RestoreStatus.RESTORING -> {
|
||||
stringResource(
|
||||
R.string.BackupStatusRow__restoring_s_of_s_s,
|
||||
backupStatusData.completedRestoredSize.toUnitString(2),
|
||||
backupStatusData.totalRestoreSize.toUnitString(2),
|
||||
"%d".format(((backupStatusData.progress ?: 0f) * 100).roundToInt())
|
||||
)
|
||||
}
|
||||
RestoreStatus.WAITING_FOR_INTERNET -> stringResource(R.string.BackupStatusRow__restore_no_internet)
|
||||
RestoreStatus.WAITING_FOR_WIFI -> stringResource(R.string.BackupStatusRow__restore_waiting_for_wifi)
|
||||
RestoreStatus.LOW_BATTERY -> stringResource(R.string.BackupStatusRow__restore_device_has_low_battery)
|
||||
RestoreStatus.FINISHED -> stringResource(R.string.BackupStatus__restore_complete)
|
||||
else -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
RestoreState.NONE -> {
|
||||
if (backupStatusData.restoreStatus == RestoreStatus.FINISHED) {
|
||||
stringResource(R.string.BackupStatus__restore_complete)
|
||||
} else {
|
||||
throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
RestoreState.PENDING,
|
||||
RestoreState.RESTORING_DB -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getDownloadingMediaString(backupStatusData: BackupStatusData.RestoringMedia): String {
|
||||
return when (backupStatusData.restoreStatus) {
|
||||
BackupStatusData.RestoreStatus.NORMAL -> {
|
||||
private fun getDownloadingMediaString(backupStatusData: ArchiveRestoreProgressState): String {
|
||||
return when (backupStatusData.restoreState) {
|
||||
RestoreState.CALCULATING_MEDIA -> {
|
||||
stringResource(
|
||||
R.string.BackupStatusRow__downloading_s_of_s_s,
|
||||
backupStatusData.bytesDownloaded.toUnitString(2),
|
||||
backupStatusData.bytesTotal.toUnitString(2),
|
||||
"%d".format((backupStatusData.progress * 100).roundToInt())
|
||||
backupStatusData.completedRestoredSize.toUnitString(2),
|
||||
backupStatusData.totalRestoreSize.toUnitString(2),
|
||||
"%d".format(((backupStatusData.progress ?: 0f) * 100).roundToInt())
|
||||
)
|
||||
}
|
||||
BackupStatusData.RestoreStatus.LOW_BATTERY -> stringResource(R.string.BackupStatusRow__download_device_has_low_battery)
|
||||
BackupStatusData.RestoreStatus.WAITING_FOR_INTERNET -> stringResource(R.string.BackupStatusRow__download_no_internet)
|
||||
BackupStatusData.RestoreStatus.WAITING_FOR_WIFI -> stringResource(R.string.BackupStatusRow__download_waiting_for_wifi)
|
||||
BackupStatusData.RestoreStatus.FINISHED -> stringResource(R.string.BackupStatus__restore_complete)
|
||||
RestoreState.CANCELING_MEDIA -> stringResource(R.string.BackupStatus__cancel_restore_media)
|
||||
RestoreState.RESTORING_MEDIA -> {
|
||||
when (backupStatusData.restoreStatus) {
|
||||
RestoreStatus.RESTORING -> {
|
||||
stringResource(
|
||||
R.string.BackupStatusRow__downloading_s_of_s_s,
|
||||
backupStatusData.completedRestoredSize.toUnitString(2),
|
||||
backupStatusData.totalRestoreSize.toUnitString(2),
|
||||
"%d".format(((backupStatusData.progress ?: 0f) * 100).roundToInt())
|
||||
)
|
||||
}
|
||||
RestoreStatus.WAITING_FOR_INTERNET -> stringResource(R.string.BackupStatusRow__download_no_internet)
|
||||
RestoreStatus.WAITING_FOR_WIFI -> stringResource(R.string.BackupStatusRow__download_waiting_for_wifi)
|
||||
RestoreStatus.LOW_BATTERY -> stringResource(R.string.BackupStatusRow__download_device_has_low_battery)
|
||||
RestoreStatus.FINISHED -> stringResource(R.string.BackupStatus__restore_complete)
|
||||
else -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
RestoreState.NONE -> {
|
||||
if (backupStatusData.restoreStatus == RestoreStatus.FINISHED) {
|
||||
stringResource(R.string.BackupStatus__restore_complete)
|
||||
} else {
|
||||
throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
RestoreState.PENDING,
|
||||
RestoreState.RESTORING_DB -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun progressColor(backupStatusData: BackupStatusData): Color {
|
||||
return if (backupStatusData is BackupStatusData.RestoringMedia && backupStatusData.restoreStatus == BackupStatusData.RestoreStatus.NORMAL) {
|
||||
MaterialTheme.colorScheme.primary
|
||||
} else {
|
||||
backupStatusData.iconColors.foreground
|
||||
private fun progressColor(backupStatusData: ArchiveRestoreProgressState): Color {
|
||||
return when (backupStatusData.restoreStatus) {
|
||||
RestoreStatus.RESTORING -> MaterialTheme.colorScheme.primary
|
||||
RestoreStatus.WAITING_FOR_INTERNET,
|
||||
RestoreStatus.WAITING_FOR_WIFI,
|
||||
RestoreStatus.LOW_BATTERY,
|
||||
RestoreStatus.NOT_ENOUGH_DISK_SPACE -> BackupsIconColors.Warning.foreground
|
||||
RestoreStatus.FINISHED -> BackupsIconColors.Success.foreground
|
||||
RestoreStatus.NONE -> BackupsIconColors.Normal.foreground
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,11 +226,7 @@ private fun progressColor(backupStatusData: BackupStatusData): Color {
|
||||
fun BackupStatusRowNormalPreview() {
|
||||
Previews.Preview {
|
||||
BackupStatusRow(
|
||||
backupStatusData = BackupStatusData.RestoringMedia(
|
||||
bytesTotal = ByteSize(100),
|
||||
bytesDownloaded = ByteSize(50),
|
||||
restoreStatus = BackupStatusData.RestoreStatus.NORMAL
|
||||
),
|
||||
backupStatusData = ArchiveRestoreProgressState(restoreState = RestoreState.RESTORING_MEDIA, restoreStatus = RestoreStatus.RESTORING, remainingRestoreSize = 800.mebiBytes, totalRestoreSize = 1024.mebiBytes),
|
||||
onCancelClick = {}
|
||||
)
|
||||
}
|
||||
@@ -284,11 +237,7 @@ fun BackupStatusRowNormalPreview() {
|
||||
fun BackupStatusRowWaitingForWifiPreview() {
|
||||
Previews.Preview {
|
||||
BackupStatusRow(
|
||||
backupStatusData = BackupStatusData.RestoringMedia(
|
||||
bytesTotal = ByteSize(100),
|
||||
bytesDownloaded = ByteSize(50),
|
||||
restoreStatus = BackupStatusData.RestoreStatus.WAITING_FOR_WIFI
|
||||
)
|
||||
backupStatusData = ArchiveRestoreProgressState(restoreState = RestoreState.RESTORING_MEDIA, restoreStatus = RestoreStatus.WAITING_FOR_WIFI, remainingRestoreSize = 800.mebiBytes, totalRestoreSize = 1024.mebiBytes)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -298,11 +247,7 @@ fun BackupStatusRowWaitingForWifiPreview() {
|
||||
fun BackupStatusRowWaitingForInternetPreview() {
|
||||
Previews.Preview {
|
||||
BackupStatusRow(
|
||||
backupStatusData = BackupStatusData.RestoringMedia(
|
||||
bytesTotal = ByteSize(100),
|
||||
bytesDownloaded = ByteSize(50),
|
||||
restoreStatus = BackupStatusData.RestoreStatus.WAITING_FOR_INTERNET
|
||||
)
|
||||
backupStatusData = ArchiveRestoreProgressState(restoreState = RestoreState.RESTORING_MEDIA, restoreStatus = RestoreStatus.WAITING_FOR_INTERNET, remainingRestoreSize = 800.mebiBytes, totalRestoreSize = 1024.mebiBytes)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -312,11 +257,7 @@ fun BackupStatusRowWaitingForInternetPreview() {
|
||||
fun BackupStatusRowLowBatteryPreview() {
|
||||
Previews.Preview {
|
||||
BackupStatusRow(
|
||||
backupStatusData = BackupStatusData.RestoringMedia(
|
||||
bytesTotal = ByteSize(100),
|
||||
bytesDownloaded = ByteSize(50),
|
||||
restoreStatus = BackupStatusData.RestoreStatus.LOW_BATTERY
|
||||
)
|
||||
backupStatusData = ArchiveRestoreProgressState(restoreState = RestoreState.RESTORING_MEDIA, restoreStatus = RestoreStatus.LOW_BATTERY, remainingRestoreSize = 800.mebiBytes, totalRestoreSize = 1024.mebiBytes)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -326,11 +267,8 @@ fun BackupStatusRowLowBatteryPreview() {
|
||||
fun BackupStatusRowFinishedPreview() {
|
||||
Previews.Preview {
|
||||
BackupStatusRow(
|
||||
backupStatusData = BackupStatusData.RestoringMedia(
|
||||
bytesTotal = ByteSize(100),
|
||||
bytesDownloaded = ByteSize(50),
|
||||
restoreStatus = BackupStatusData.RestoreStatus.FINISHED
|
||||
)
|
||||
backupStatusData = ArchiveRestoreProgressState(restoreState = RestoreState.NONE, restoreStatus = RestoreStatus.FINISHED, remainingRestoreSize = 0.mebiBytes, totalRestoreSize = 0.mebiBytes, totalToRestoreThisRun = 1024.mebiBytes),
|
||||
onCancelClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -340,29 +278,7 @@ fun BackupStatusRowFinishedPreview() {
|
||||
fun BackupStatusRowNotEnoughFreeSpacePreview() {
|
||||
Previews.Preview {
|
||||
BackupStatusRow(
|
||||
backupStatusData = BackupStatusData.NotEnoughFreeSpace(
|
||||
requiredSpace = ByteSize(50)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
fun BackupStatusRowCouldNotCompleteBackupPreview() {
|
||||
Previews.Preview {
|
||||
BackupStatusRow(
|
||||
backupStatusData = BackupStatusData.CouldNotCompleteBackup
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
fun BackupStatusRowBackupFailedPreview() {
|
||||
Previews.Preview {
|
||||
BackupStatusRow(
|
||||
backupStatusData = BackupStatusData.BackupFailed
|
||||
backupStatusData = ArchiveRestoreProgressState(restoreState = RestoreState.RESTORING_MEDIA, restoreStatus = RestoreStatus.NOT_ENOUGH_DISK_SPACE, remainingRestoreSize = 500.mebiBytes, totalRestoreSize = 1024.mebiBytes)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,143 +5,53 @@
|
||||
|
||||
package org.thoughtcrime.securesms.banner.banners
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.net.ConnectivityManager
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.runtime.Composable
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.signal.core.util.bytes
|
||||
import org.signal.core.util.throttleLatest
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import org.thoughtcrime.securesms.backup.v2.ArchiveRestoreProgress
|
||||
import org.thoughtcrime.securesms.backup.v2.ArchiveRestoreProgressState
|
||||
import org.thoughtcrime.securesms.backup.v2.ArchiveRestoreProgressState.RestoreStatus
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.status.BackupStatusBanner
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.status.BackupStatusData
|
||||
import org.thoughtcrime.securesms.banner.Banner
|
||||
import org.thoughtcrime.securesms.database.DatabaseObserver
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.BatteryNotLowConstraint
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.WifiConstraint
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.util.safeUnregisterReceiver
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class MediaRestoreProgressBanner(private val listener: RestoreProgressBannerListener = EmptyListener) : Banner<BackupStatusData>() {
|
||||
|
||||
private var totalRestoredSize: Long = 0
|
||||
class MediaRestoreProgressBanner(private val listener: RestoreProgressBannerListener = EmptyListener) : Banner<ArchiveRestoreProgressState>() {
|
||||
|
||||
override val enabled: Boolean
|
||||
get() = SignalStore.backup.isMediaRestoreInProgress || totalRestoredSize > 0
|
||||
get() = ArchiveRestoreProgress.state.let { it.restoreState.isMediaRestoreOperation || it.restoreStatus == RestoreStatus.FINISHED }
|
||||
|
||||
override val dataFlow: Flow<BackupStatusData> by lazy {
|
||||
SignalStore
|
||||
.backup
|
||||
.totalRestorableAttachmentSizeFlow
|
||||
.flatMapLatest { size ->
|
||||
when {
|
||||
size > 0 -> {
|
||||
totalRestoredSize = size
|
||||
getActiveRestoreFlow()
|
||||
}
|
||||
|
||||
totalRestoredSize > 0 -> {
|
||||
flowOf(
|
||||
BackupStatusData.RestoringMedia(
|
||||
bytesTotal = totalRestoredSize.bytes,
|
||||
restoreStatus = BackupStatusData.RestoreStatus.FINISHED
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
else -> flowOf(BackupStatusData.RestoringMedia())
|
||||
}
|
||||
override val dataFlow: Flow<ArchiveRestoreProgressState> by lazy {
|
||||
ArchiveRestoreProgress
|
||||
.stateFlow
|
||||
.filter {
|
||||
it.restoreStatus != RestoreStatus.NONE && (it.restoreState.isMediaRestoreOperation || it.restoreStatus == RestoreStatus.FINISHED)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun DisplayBanner(model: BackupStatusData, contentPadding: PaddingValues) {
|
||||
override fun DisplayBanner(model: ArchiveRestoreProgressState, contentPadding: PaddingValues) {
|
||||
BackupStatusBanner(
|
||||
data = model,
|
||||
onBannerClick = listener::onBannerClick,
|
||||
onActionClick = listener::onActionClick,
|
||||
onDismissClick = {
|
||||
totalRestoredSize = 0
|
||||
ArchiveRestoreProgress.clearFinishedStatus()
|
||||
listener.onDismissComplete()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun getActiveRestoreFlow(): Flow<BackupStatusData> {
|
||||
val flow: Flow<Unit> = callbackFlow {
|
||||
val onChange = { trySend(Unit) }
|
||||
|
||||
val observer = DatabaseObserver.Observer {
|
||||
onChange()
|
||||
}
|
||||
|
||||
val receiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
onChange()
|
||||
}
|
||||
}
|
||||
|
||||
onChange()
|
||||
|
||||
AppDependencies.databaseObserver.registerAttachmentUpdatedObserver(observer)
|
||||
AppDependencies.application.registerReceiver(receiver, IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION))
|
||||
AppDependencies.application.registerReceiver(receiver, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
|
||||
|
||||
awaitClose {
|
||||
AppDependencies.databaseObserver.unregisterObserver(observer)
|
||||
AppDependencies.application.safeUnregisterReceiver(receiver)
|
||||
}
|
||||
}
|
||||
|
||||
return flow
|
||||
.throttleLatest(1.seconds)
|
||||
.map {
|
||||
val totalRestoreSize = SignalStore.backup.totalRestorableAttachmentSize
|
||||
val remainingAttachmentSize = SignalDatabase.attachments.getRemainingRestorableAttachmentSize()
|
||||
val completedBytes = totalRestoreSize - remainingAttachmentSize
|
||||
|
||||
when {
|
||||
!WifiConstraint.isMet(AppDependencies.application) && !SignalStore.backup.restoreWithCellular -> BackupStatusData.RestoringMedia(completedBytes.bytes, totalRestoreSize.bytes, BackupStatusData.RestoreStatus.WAITING_FOR_WIFI)
|
||||
!NetworkConstraint.isMet(AppDependencies.application) -> BackupStatusData.RestoringMedia(completedBytes.bytes, totalRestoreSize.bytes, BackupStatusData.RestoreStatus.WAITING_FOR_INTERNET)
|
||||
!BatteryNotLowConstraint.isMet() -> BackupStatusData.RestoringMedia(completedBytes.bytes, totalRestoreSize.bytes, BackupStatusData.RestoreStatus.LOW_BATTERY)
|
||||
else -> {
|
||||
val availableBytes = SignalStore.backup.spaceAvailableOnDiskBytes
|
||||
|
||||
if (availableBytes > -1L && remainingAttachmentSize > availableBytes) {
|
||||
BackupStatusData.NotEnoughFreeSpace(requiredSpace = remainingAttachmentSize.bytes)
|
||||
} else {
|
||||
BackupStatusData.RestoringMedia(completedBytes.bytes, totalRestoreSize.bytes)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.flowOn(Dispatchers.IO)
|
||||
}
|
||||
|
||||
interface RestoreProgressBannerListener {
|
||||
fun onBannerClick()
|
||||
fun onActionClick(data: BackupStatusData)
|
||||
fun onActionClick(data: ArchiveRestoreProgressState)
|
||||
fun onDismissComplete()
|
||||
}
|
||||
|
||||
private object EmptyListener : RestoreProgressBannerListener {
|
||||
override fun onBannerClick() = Unit
|
||||
override fun onActionClick(data: BackupStatusData) = Unit
|
||||
override fun onActionClick(data: ArchiveRestoreProgressState) = Unit
|
||||
override fun onDismissComplete() = Unit
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,17 +5,13 @@
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.backups.remote
|
||||
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.status.BackupStatusData
|
||||
import org.thoughtcrime.securesms.backup.v2.ArchiveRestoreProgressState
|
||||
|
||||
/**
|
||||
* State container for BackupStatusData, including the enabled state.
|
||||
*/
|
||||
sealed interface BackupRestoreState {
|
||||
data object None : BackupRestoreState
|
||||
data class Ready(
|
||||
val bytes: String
|
||||
) : BackupRestoreState
|
||||
data class FromBackupStatusData(
|
||||
val backupStatusData: BackupStatusData
|
||||
) : BackupRestoreState
|
||||
data class Ready(val bytes: String) : BackupRestoreState
|
||||
data class Restoring(val state: ArchiveRestoreProgressState) : BackupRestoreState
|
||||
}
|
||||
|
||||
@@ -90,11 +90,14 @@ import org.thoughtcrime.securesms.DevicePinAuthEducationSheet
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.backup.ArchiveUploadProgress
|
||||
import org.thoughtcrime.securesms.backup.DeletionState
|
||||
import org.thoughtcrime.securesms.backup.RestoreState
|
||||
import org.thoughtcrime.securesms.backup.v2.ArchiveRestoreProgressState
|
||||
import org.thoughtcrime.securesms.backup.v2.ArchiveRestoreProgressState.RestoreStatus
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupFrequency
|
||||
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.BackupAlert
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.BackupAlertBottomSheet
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.status.BackupStatusData
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.status.BackupCreateErrorRow
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.status.BackupStatusRow
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.status.RestoreType
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
|
||||
@@ -222,7 +225,7 @@ class RemoteBackupsSettingsFragment : ComposeFragment() {
|
||||
}
|
||||
|
||||
override fun onCancelMediaRestore() {
|
||||
viewModel.requestDialog(RemoteBackupsSettingsState.Dialog.CANCEL_MEDIA_RESTORE_PROTECTION)
|
||||
viewModel.cancelMediaRestore()
|
||||
}
|
||||
|
||||
override fun onDisplaySkipMediaRestoreProtectionDialog() {
|
||||
@@ -512,28 +515,28 @@ private fun RemoteBackupsSettingsContent(
|
||||
)
|
||||
} else if (state.backupsEnabled) {
|
||||
appendBackupDetailsItems(
|
||||
backupState = state.backupState,
|
||||
canViewBackupKey = state.canViewBackupKey,
|
||||
state = state,
|
||||
backupRestoreState = backupRestoreState,
|
||||
backupProgress = backupProgress,
|
||||
canBackupMessagesRun = state.canBackupMessagesJobRun,
|
||||
lastBackupTimestamp = state.lastBackupTimestamp,
|
||||
backupMediaSize = state.backupMediaSize,
|
||||
canBackUpUsingCellular = state.canBackUpUsingCellular,
|
||||
canRestoreUsingCellular = state.canRestoreUsingCellular,
|
||||
canBackUpNow = !state.isOutOfStorageSpace,
|
||||
includeDebuglog = state.includeDebuglog,
|
||||
backupMediaDetails = state.backupMediaDetails,
|
||||
contentCallbacks = contentCallbacks
|
||||
)
|
||||
} else {
|
||||
if (backupRestoreState is BackupRestoreState.FromBackupStatusData) {
|
||||
if (state.showBackupCreateFailedError || state.showBackupCreateCouldNotCompleteError) {
|
||||
item {
|
||||
BackupCreateErrorRow(
|
||||
showCouldNotComplete = state.showBackupCreateCouldNotCompleteError,
|
||||
showBackupFailed = state.showBackupCreateFailedError,
|
||||
onLearnMoreClick = contentCallbacks::onLearnMoreAboutBackupFailure
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (backupRestoreState is BackupRestoreState.Restoring) {
|
||||
item {
|
||||
BackupStatusRow(
|
||||
backupStatusData = backupRestoreState.backupStatusData,
|
||||
backupStatusData = backupRestoreState.state,
|
||||
onCancelClick = contentCallbacks::onCancelMediaRestore,
|
||||
onSkipClick = contentCallbacks::onSkipMediaRestore,
|
||||
onLearnMoreClick = contentCallbacks::onLearnMoreAboutBackupFailure
|
||||
onSkipClick = contentCallbacks::onSkipMediaRestore
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -665,24 +668,24 @@ private fun ReenableBackupsButton(contentCallbacks: ContentCallbacks) {
|
||||
}
|
||||
|
||||
private fun LazyListScope.appendRestoreFromBackupStatusData(
|
||||
backupRestoreState: BackupRestoreState.FromBackupStatusData,
|
||||
backupRestoreState: BackupRestoreState.Restoring,
|
||||
canRestoreUsingCellular: Boolean,
|
||||
contentCallbacks: ContentCallbacks,
|
||||
isCancelable: Boolean = true
|
||||
) {
|
||||
item {
|
||||
BackupStatusRow(
|
||||
backupStatusData = backupRestoreState.backupStatusData,
|
||||
backupStatusData = backupRestoreState.state,
|
||||
restoreType = if (isCancelable) RestoreType.DOWNLOAD else RestoreType.RESTORE,
|
||||
onCancelClick = if (isCancelable) contentCallbacks::onCancelMediaRestore else null,
|
||||
onSkipClick = contentCallbacks::onDisplaySkipMediaRestoreProtectionDialog,
|
||||
onLearnMoreClick = contentCallbacks::onLearnMoreAboutBackupFailure
|
||||
onSkipClick = contentCallbacks::onDisplaySkipMediaRestoreProtectionDialog
|
||||
)
|
||||
}
|
||||
|
||||
val displayResumeButton = when (val data = backupRestoreState.backupStatusData) {
|
||||
is BackupStatusData.RestoringMedia -> !canRestoreUsingCellular && data.restoreStatus == BackupStatusData.RestoreStatus.WAITING_FOR_WIFI
|
||||
else -> false
|
||||
val displayResumeButton = if (backupRestoreState.state.restoreState == RestoreState.RESTORING_MEDIA) {
|
||||
!canRestoreUsingCellular && backupRestoreState.state.restoreStatus == RestoreStatus.WAITING_FOR_WIFI
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
if (displayResumeButton) {
|
||||
@@ -744,7 +747,7 @@ private fun LazyListScope.appendBackupDeletionItems(
|
||||
)
|
||||
}
|
||||
|
||||
if (backupRestoreState is BackupRestoreState.FromBackupStatusData) {
|
||||
if (backupRestoreState is BackupRestoreState.Restoring) {
|
||||
appendRestoreFromBackupStatusData(
|
||||
backupRestoreState = backupRestoreState,
|
||||
canRestoreUsingCellular = canRestoreUsingCellular,
|
||||
@@ -754,7 +757,9 @@ private fun LazyListScope.appendBackupDeletionItems(
|
||||
} else {
|
||||
item {
|
||||
LinearProgressIndicator(
|
||||
modifier = Modifier.horizontalGutters().fillMaxWidth()
|
||||
modifier = Modifier
|
||||
.horizontalGutters()
|
||||
.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -819,18 +824,9 @@ private fun DescriptionText(
|
||||
}
|
||||
|
||||
private fun LazyListScope.appendBackupDetailsItems(
|
||||
backupState: BackupState,
|
||||
canViewBackupKey: Boolean,
|
||||
state: RemoteBackupsSettingsState,
|
||||
backupRestoreState: BackupRestoreState,
|
||||
backupProgress: ArchiveUploadProgressState?,
|
||||
canBackupMessagesRun: Boolean,
|
||||
lastBackupTimestamp: Long,
|
||||
backupMediaSize: Long,
|
||||
canBackUpUsingCellular: Boolean,
|
||||
canRestoreUsingCellular: Boolean,
|
||||
canBackUpNow: Boolean,
|
||||
includeDebuglog: Boolean?,
|
||||
backupMediaDetails: RemoteBackupsSettingsState.BackupMediaDetails?,
|
||||
contentCallbacks: ContentCallbacks
|
||||
) {
|
||||
item {
|
||||
@@ -841,45 +837,55 @@ private fun LazyListScope.appendBackupDetailsItems(
|
||||
Texts.SectionHeader(text = stringResource(id = R.string.RemoteBackupsSettingsFragment__backup_details))
|
||||
}
|
||||
|
||||
if (backupMediaDetails != null) {
|
||||
if (state.backupMediaDetails != null) {
|
||||
item {
|
||||
Column(modifier = Modifier.horizontalGutters()) {
|
||||
Text("[Internal Only] Backup Media Details")
|
||||
Text("Awaiting Restore: ${backupMediaDetails.awaitingRestore.toUnitString()}")
|
||||
Text("Offloaded: ${backupMediaDetails.offloaded.toUnitString()}")
|
||||
Text("Awaiting Restore: ${state.backupMediaDetails.awaitingRestore.toUnitString()}")
|
||||
Text("Offloaded: ${state.backupMediaDetails.offloaded.toUnitString()}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (state.showBackupCreateFailedError || state.showBackupCreateCouldNotCompleteError) {
|
||||
item {
|
||||
BackupCreateErrorRow(
|
||||
showCouldNotComplete = state.showBackupCreateCouldNotCompleteError,
|
||||
showBackupFailed = state.showBackupCreateFailedError,
|
||||
onLearnMoreClick = contentCallbacks::onLearnMoreAboutBackupFailure
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (backupRestoreState !is BackupRestoreState.None) {
|
||||
if (backupRestoreState is BackupRestoreState.FromBackupStatusData) {
|
||||
if (backupRestoreState is BackupRestoreState.Restoring) {
|
||||
appendRestoreFromBackupStatusData(
|
||||
backupRestoreState = backupRestoreState,
|
||||
canRestoreUsingCellular = canRestoreUsingCellular,
|
||||
canRestoreUsingCellular = state.canRestoreUsingCellular,
|
||||
contentCallbacks = contentCallbacks
|
||||
)
|
||||
} else if (backupRestoreState is BackupRestoreState.Ready) {
|
||||
item {
|
||||
BackupReadyToDownloadRow(
|
||||
ready = backupRestoreState,
|
||||
backupState = backupState,
|
||||
backupState = state.backupState,
|
||||
onDownloadClick = contentCallbacks::onStartMediaRestore
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (includeDebuglog != null) {
|
||||
if (state.includeDebuglog != null) {
|
||||
item {
|
||||
IncludeDebuglogRow(includeDebuglog) { contentCallbacks.onIncludeDebuglogClick(it) }
|
||||
IncludeDebuglogRow(state.includeDebuglog) { contentCallbacks.onIncludeDebuglogClick(it) }
|
||||
}
|
||||
}
|
||||
|
||||
if (backupProgress == null || backupProgress.state == ArchiveUploadProgressState.State.None || backupProgress.state == ArchiveUploadProgressState.State.UserCanceled) {
|
||||
item {
|
||||
LastBackupRow(
|
||||
lastBackupTimestamp = lastBackupTimestamp,
|
||||
enabled = canBackUpNow,
|
||||
lastBackupTimestamp = state.lastBackupTimestamp,
|
||||
enabled = !state.isOutOfStorageSpace,
|
||||
onBackupNowClick = contentCallbacks::onBackupNowClick
|
||||
)
|
||||
}
|
||||
@@ -887,19 +893,19 @@ private fun LazyListScope.appendBackupDetailsItems(
|
||||
item {
|
||||
InProgressBackupRow(
|
||||
archiveUploadProgressState = backupProgress,
|
||||
canBackupMessagesRun = canBackupMessagesRun,
|
||||
canBackupUsingCellular = canBackUpUsingCellular,
|
||||
canBackupMessagesRun = state.canBackupMessagesJobRun,
|
||||
canBackupUsingCellular = state.canBackUpUsingCellular,
|
||||
cancelArchiveUpload = contentCallbacks::onCancelUploadClick
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (backupState !is BackupState.ActiveFree) {
|
||||
if (state.backupState !is BackupState.ActiveFree) {
|
||||
item {
|
||||
val sizeText = if (backupMediaSize < 0L) {
|
||||
val sizeText = if (state.backupMediaSize < 0L) {
|
||||
stringResource(R.string.RemoteBackupsSettingsFragment__calculating)
|
||||
} else {
|
||||
backupMediaSize.bytes.toUnitString()
|
||||
state.backupMediaSize.bytes.toUnitString()
|
||||
}
|
||||
|
||||
Rows.TextRow(text = {
|
||||
@@ -941,7 +947,7 @@ private fun LazyListScope.appendBackupDetailsItems(
|
||||
|
||||
item {
|
||||
Rows.ToggleRow(
|
||||
checked = canBackUpUsingCellular,
|
||||
checked = state.canBackUpUsingCellular,
|
||||
text = stringResource(id = R.string.RemoteBackupsSettingsFragment__back_up_using_cellular),
|
||||
onCheckChanged = contentCallbacks::onBackUpUsingCellularClick
|
||||
)
|
||||
@@ -951,7 +957,7 @@ private fun LazyListScope.appendBackupDetailsItems(
|
||||
Rows.TextRow(
|
||||
text = stringResource(R.string.RemoteBackupsSettingsFragment__view_backup_key),
|
||||
onClick = contentCallbacks::onViewBackupKeyClick,
|
||||
enabled = canViewBackupKey
|
||||
enabled = state.canViewBackupKey
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1112,13 +1118,17 @@ private fun OutOfStorageSpaceBlock(
|
||||
Dividers.Default()
|
||||
|
||||
Row(
|
||||
modifier = Modifier.horizontalGutters().padding(vertical = 12.dp)
|
||||
modifier = Modifier
|
||||
.horizontalGutters()
|
||||
.padding(vertical = 12.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.symbol_error_circle_fill_24),
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.padding(top = 4.dp, end = 4.dp, start = 2.dp).size(20.dp)
|
||||
modifier = Modifier
|
||||
.padding(top = 4.dp, end = 4.dp, start = 2.dp)
|
||||
.size(20.dp)
|
||||
)
|
||||
|
||||
Column {
|
||||
@@ -1716,7 +1726,7 @@ private fun RemoteBackupsSettingsContentPreview() {
|
||||
),
|
||||
statusBarColorNestedScrollConnection = null,
|
||||
backupDeleteState = DeletionState.NONE,
|
||||
backupRestoreState = BackupRestoreState.FromBackupStatusData(BackupStatusData.CouldNotCompleteBackup),
|
||||
backupRestoreState = BackupRestoreState.None,
|
||||
contentCallbacks = ContentCallbacks.Empty,
|
||||
backupProgress = null
|
||||
)
|
||||
@@ -1745,7 +1755,7 @@ private fun RemoteBackupsSettingsInternalUserContentPreview() {
|
||||
),
|
||||
statusBarColorNestedScrollConnection = null,
|
||||
backupDeleteState = DeletionState.NONE,
|
||||
backupRestoreState = BackupRestoreState.FromBackupStatusData(BackupStatusData.CouldNotCompleteBackup),
|
||||
backupRestoreState = BackupRestoreState.None,
|
||||
contentCallbacks = ContentCallbacks.Empty,
|
||||
backupProgress = null
|
||||
)
|
||||
@@ -2047,11 +2057,8 @@ private fun BackupDeletionCardPreview() {
|
||||
for (state in DeletionState.entries.filter { it.hasUx() }) {
|
||||
appendBackupDeletionItems(
|
||||
backupDeleteState = state,
|
||||
backupRestoreState = BackupRestoreState.FromBackupStatusData(
|
||||
backupStatusData = BackupStatusData.RestoringMedia(
|
||||
bytesDownloaded = 80.mebiBytes,
|
||||
bytesTotal = 3.gibiBytes
|
||||
)
|
||||
backupRestoreState = BackupRestoreState.Restoring(
|
||||
state = ArchiveRestoreProgressState(restoreState = RestoreState.RESTORING_MEDIA, restoreStatus = RestoreStatus.RESTORING, remainingRestoreSize = 800.mebiBytes, totalRestoreSize = 1024.mebiBytes)
|
||||
),
|
||||
contentCallbacks = ContentCallbacks.Empty,
|
||||
canRestoreUsingCellular = true
|
||||
|
||||
@@ -28,7 +28,9 @@ data class RemoteBackupsSettingsState(
|
||||
val snackbar: Snackbar = Snackbar.NONE,
|
||||
val includeDebuglog: Boolean? = null,
|
||||
val canBackupMessagesJobRun: Boolean = false,
|
||||
val backupMediaDetails: BackupMediaDetails? = null
|
||||
val backupMediaDetails: BackupMediaDetails? = null,
|
||||
val showBackupCreateFailedError: Boolean = false,
|
||||
val showBackupCreateCouldNotCompleteError: Boolean = false
|
||||
) {
|
||||
|
||||
data class BackupMediaDetails(
|
||||
|
||||
@@ -30,10 +30,10 @@ import org.signal.core.util.throttleLatest
|
||||
import org.signal.donations.InAppPaymentType
|
||||
import org.thoughtcrime.securesms.backup.ArchiveUploadProgress
|
||||
import org.thoughtcrime.securesms.backup.DeletionState
|
||||
import org.thoughtcrime.securesms.backup.v2.ArchiveRestoreProgress
|
||||
import org.thoughtcrime.securesms.backup.v2.ArchiveRestoreProgressState.RestoreStatus
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
||||
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.status.BackupStatusData
|
||||
import org.thoughtcrime.securesms.banner.banners.MediaRestoreProgressBanner
|
||||
import org.thoughtcrime.securesms.components.settings.app.backups.BackupStateObserver
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
@@ -70,7 +70,9 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
|
||||
lastBackupTimestamp = SignalStore.backup.lastBackupTime,
|
||||
canBackUpUsingCellular = SignalStore.backup.backupWithCellular,
|
||||
canRestoreUsingCellular = SignalStore.backup.restoreWithCellular,
|
||||
includeDebuglog = SignalStore.internal.includeDebuglogInBackup.takeIf { RemoteConfig.internalUser }
|
||||
includeDebuglog = SignalStore.internal.includeDebuglogInBackup.takeIf { RemoteConfig.internalUser },
|
||||
showBackupCreateFailedError = BackupRepository.shouldDisplayBackupFailedSettingsRow(),
|
||||
showBackupCreateCouldNotCompleteError = BackupRepository.shouldDisplayCouldNotCompleteBackupSettingsRow()
|
||||
)
|
||||
)
|
||||
|
||||
@@ -111,16 +113,14 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val restoreProgress = MediaRestoreProgressBanner()
|
||||
|
||||
var optimizedRemainingBytes = 0L
|
||||
while (isActive) {
|
||||
if (restoreProgress.enabled) {
|
||||
if (ArchiveRestoreProgress.state.let { it.restoreState.isMediaRestoreOperation || it.restoreStatus == RestoreStatus.FINISHED }) {
|
||||
Log.d(TAG, "Backup is being restored. Collecting updates.")
|
||||
restoreProgress
|
||||
.dataFlow
|
||||
.onEach { latest -> _restoreState.update { BackupRestoreState.FromBackupStatusData(latest) } }
|
||||
.takeWhile { it !is BackupStatusData.RestoringMedia || it.restoreStatus != BackupStatusData.RestoreStatus.FINISHED }
|
||||
ArchiveRestoreProgress
|
||||
.stateFlow
|
||||
.takeWhile { it.restoreState.isMediaRestoreOperation || it.restoreStatus == RestoreStatus.FINISHED }
|
||||
.onEach { latest -> _restoreState.update { BackupRestoreState.Restoring(latest) } }
|
||||
.collect()
|
||||
} else if (
|
||||
!SignalStore.backup.optimizeStorage &&
|
||||
@@ -130,10 +130,6 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
|
||||
_restoreState.update { BackupRestoreState.Ready(optimizedRemainingBytes.bytes.toUnitString()) }
|
||||
} else if (SignalStore.backup.totalRestorableAttachmentSize > 0L) {
|
||||
_restoreState.update { BackupRestoreState.Ready(SignalStore.backup.totalRestorableAttachmentSize.bytes.toUnitString()) }
|
||||
} else if (BackupRepository.shouldDisplayBackupFailedSettingsRow()) {
|
||||
_restoreState.update { BackupRestoreState.FromBackupStatusData(BackupStatusData.BackupFailed) }
|
||||
} else if (BackupRepository.shouldDisplayCouldNotCompleteBackupSettingsRow()) {
|
||||
_restoreState.update { BackupRestoreState.FromBackupStatusData(BackupStatusData.CouldNotCompleteBackup) }
|
||||
} else {
|
||||
_restoreState.update { BackupRestoreState.None }
|
||||
}
|
||||
@@ -186,6 +182,14 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
|
||||
BackupRepository.resumeMediaRestore()
|
||||
}
|
||||
|
||||
fun cancelMediaRestore() {
|
||||
if (ArchiveRestoreProgress.state.restoreStatus == RestoreStatus.FINISHED) {
|
||||
ArchiveRestoreProgress.clearFinishedStatus()
|
||||
} else {
|
||||
requestDialog(RemoteBackupsSettingsState.Dialog.CANCEL_MEDIA_RESTORE_PROTECTION)
|
||||
}
|
||||
}
|
||||
|
||||
fun skipMediaRestore() {
|
||||
BackupRepository.skipMediaRestore()
|
||||
|
||||
@@ -295,7 +299,9 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
|
||||
canBackUpUsingCellular = SignalStore.backup.backupWithCellular,
|
||||
canRestoreUsingCellular = SignalStore.backup.restoreWithCellular,
|
||||
isOutOfStorageSpace = BackupRepository.shouldDisplayOutOfRemoteStorageSpaceUx(),
|
||||
hasRedemptionError = lastPurchase?.data?.error?.data_ == "409"
|
||||
hasRedemptionError = lastPurchase?.data?.error?.data_ == "409",
|
||||
showBackupCreateFailedError = BackupRepository.shouldDisplayBackupFailedSettingsRow(),
|
||||
showBackupCreateCouldNotCompleteError = BackupRepository.shouldDisplayCouldNotCompleteBackupSettingsRow()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +68,7 @@ import org.signal.core.ui.compose.TextFields.TextField
|
||||
import org.signal.core.util.Base64
|
||||
import org.signal.core.util.Hex
|
||||
import org.signal.core.util.getLength
|
||||
import org.thoughtcrime.securesms.MainActivity
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
||||
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
||||
@@ -190,7 +191,12 @@ class InternalBackupPlaygroundFragment : ComposeFragment() {
|
||||
MaterialAlertDialogBuilder(context)
|
||||
.setTitle("Are you sure?")
|
||||
.setMessage("This will delete all of your chats! Make sure you've finished a backup first, we don't check for you. Only do this on a test device!")
|
||||
.setPositiveButton("Wipe and restore") { _, _ -> viewModel.wipeAllDataAndRestoreFromRemote() }
|
||||
.setPositiveButton("Wipe and restore") { _, _ ->
|
||||
Toast.makeText(this@InternalBackupPlaygroundFragment.requireContext(), "Restoring backup...", Toast.LENGTH_SHORT).show()
|
||||
viewModel.wipeAllDataAndRestoreFromRemote {
|
||||
startActivity(MainActivity.clearTop(this@InternalBackupPlaygroundFragment.requireActivity()))
|
||||
}
|
||||
}
|
||||
.show()
|
||||
},
|
||||
onImportEncryptedBackupFromDiskClicked = {
|
||||
|
||||
@@ -24,6 +24,7 @@ import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.signal.core.util.Hex
|
||||
import org.signal.core.util.ThreadUtil
|
||||
import org.signal.core.util.bytes
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.signal.core.util.copyTo
|
||||
@@ -305,10 +306,10 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
fun wipeAllDataAndRestoreFromRemote() {
|
||||
fun wipeAllDataAndRestoreFromRemote(afterDbRestoreCallback: () -> Unit) {
|
||||
SignalExecutors.BOUNDED_IO.execute {
|
||||
SignalStore.backup.restoreWithCellular = false
|
||||
restoreFromRemote()
|
||||
restoreFromRemote(afterDbRestoreCallback)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -352,12 +353,15 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
|
||||
_state.value = _state.value.copy(dialog = DialogState.None)
|
||||
}
|
||||
|
||||
private fun restoreFromRemote() {
|
||||
private fun restoreFromRemote(afterDbRestoreCallback: () -> Unit) {
|
||||
_state.value = _state.value.copy(statusMessage = "Importing from remote...")
|
||||
|
||||
viewModelScope.launch {
|
||||
when (val result = BackupRepository.restoreRemoteBackup()) {
|
||||
RemoteRestoreResult.Success -> _state.value = _state.value.copy(statusMessage = "Import complete!")
|
||||
RemoteRestoreResult.Success -> {
|
||||
_state.value = _state.value.copy(statusMessage = "Import complete!")
|
||||
ThreadUtil.runOnMain { afterDbRestoreCallback() }
|
||||
}
|
||||
RemoteRestoreResult.Canceled,
|
||||
RemoteRestoreResult.Failure,
|
||||
RemoteRestoreResult.PermanentSvrBFailure,
|
||||
|
||||
@@ -10,6 +10,7 @@ import org.signal.core.util.Stopwatch
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.toInt
|
||||
import org.signal.paging.PagedDataSource
|
||||
import org.thoughtcrime.securesms.backup.v2.ArchiveRestoreProgress
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupRestoreManager
|
||||
import org.thoughtcrime.securesms.conversation.ConversationData
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage
|
||||
@@ -21,7 +22,6 @@ import org.thoughtcrime.securesms.database.model.InMemoryMessageRecord.Universal
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.messagerequests.MessageRequestRepository
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||
@@ -125,7 +125,7 @@ class ConversationDataSource(
|
||||
records = MessageDataFetcher.updateModelsWithData(records, extraData).toMutableList()
|
||||
stopwatch.split("models")
|
||||
|
||||
if (RemoteConfig.messageBackups && SignalStore.backup.restoreState.inProgress) {
|
||||
if (RemoteConfig.messageBackups && ArchiveRestoreProgress.state.activelyRestoring()) {
|
||||
BackupRestoreManager.prioritizeAttachmentsIfNeeded(records)
|
||||
stopwatch.split("restore")
|
||||
}
|
||||
|
||||
@@ -76,10 +76,12 @@ import org.thoughtcrime.securesms.MainFragment;
|
||||
import org.thoughtcrime.securesms.MainNavigator;
|
||||
import org.thoughtcrime.securesms.MuteDialog;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.backup.RestoreState;
|
||||
import org.thoughtcrime.securesms.backup.v2.ArchiveRestoreProgress;
|
||||
import org.thoughtcrime.securesms.backup.v2.ArchiveRestoreProgressState;
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.BackupAlert;
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.BackupAlertBottomSheet;
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.BackupAlertDelegate;
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.status.BackupStatusData;
|
||||
import org.thoughtcrime.securesms.badges.models.Badge;
|
||||
import org.thoughtcrime.securesms.badges.self.expired.ExpiredOneTimeBadgeBottomSheetDialogFragment;
|
||||
import org.thoughtcrime.securesms.badges.self.expired.MonthlyDonationCanceledBottomSheetDialogFragment;
|
||||
@@ -94,9 +96,9 @@ import org.thoughtcrime.securesms.banner.banners.OutdatedBuildBanner;
|
||||
import org.thoughtcrime.securesms.banner.banners.ServiceOutageBanner;
|
||||
import org.thoughtcrime.securesms.banner.banners.UnauthorizedBanner;
|
||||
import org.thoughtcrime.securesms.banner.banners.UsernameOutOfSyncBanner;
|
||||
import org.thoughtcrime.securesms.components.compose.DeleteSyncEducationDialog;
|
||||
import org.thoughtcrime.securesms.components.RatingManager;
|
||||
import org.thoughtcrime.securesms.components.SignalProgressDialog;
|
||||
import org.thoughtcrime.securesms.components.compose.DeleteSyncEducationDialog;
|
||||
import org.thoughtcrime.securesms.components.menu.ActionItem;
|
||||
import org.thoughtcrime.securesms.components.menu.SignalBottomActionBar;
|
||||
import org.thoughtcrime.securesms.components.menu.SignalContextMenu;
|
||||
@@ -746,16 +748,20 @@ public class ConversationListFragment extends MainFragment implements Conversati
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActionClick(@NonNull BackupStatusData backupStatusData) {
|
||||
if (backupStatusData instanceof BackupStatusData.NotEnoughFreeSpace) {
|
||||
BackupAlertBottomSheet.create(new BackupAlert.DiskFull(((BackupStatusData.NotEnoughFreeSpace) backupStatusData).getRequiredSpace()))
|
||||
.show(getParentFragmentManager(), null);
|
||||
} else if (backupStatusData instanceof BackupStatusData.RestoringMedia && ((BackupStatusData.RestoringMedia) backupStatusData).getRestoreStatus() == BackupStatusData.RestoreStatus.WAITING_FOR_WIFI) {
|
||||
public void onActionClick(@NonNull ArchiveRestoreProgressState data) {
|
||||
if (data.getRestoreStatus() == ArchiveRestoreProgressState.RestoreStatus.NOT_ENOUGH_DISK_SPACE) {
|
||||
BackupAlertBottomSheet.create(new BackupAlert.DiskFull(data.getRemainingRestoreSize().toUnitString())).show(getParentFragmentManager(), null);
|
||||
} else if (data.getRestoreState() == RestoreState.RESTORING_MEDIA && data.getRestoreStatus() == ArchiveRestoreProgressState.RestoreStatus.WAITING_FOR_WIFI) {
|
||||
new MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.ResumeRestoreCellular_resume_using_cellular_title)
|
||||
.setMessage(R.string.ResumeRestoreCellular_resume_using_cellular_message)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(R.string.BackupStatus__resume, (d, w) -> SignalStore.backup().setRestoreWithCellular(true))
|
||||
.setPositiveButton(R.string.BackupStatus__resume, (d, w) -> {
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
SignalStore.backup().setRestoreWithCellular(true);
|
||||
ArchiveRestoreProgress.forceUpdate();
|
||||
});
|
||||
})
|
||||
.show();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,8 +20,8 @@ import org.signal.libsignal.net.SvrBStoreResponse
|
||||
import org.signal.protos.resumableuploads.ResumableUpload
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.backup.ArchiveUploadProgress
|
||||
import org.thoughtcrime.securesms.backup.RestoreState
|
||||
import org.thoughtcrime.securesms.backup.v2.ArchiveMediaItemIterator
|
||||
import org.thoughtcrime.securesms.backup.v2.ArchiveRestoreProgress
|
||||
import org.thoughtcrime.securesms.backup.v2.ArchiveValidator
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
||||
import org.thoughtcrime.securesms.backup.v2.ResumableMessagesBackupUploadSpec
|
||||
@@ -80,12 +80,7 @@ class BackupMessagesJob private constructor(
|
||||
false
|
||||
}
|
||||
|
||||
SignalStore.backup.restoreState == RestoreState.PENDING -> {
|
||||
Log.i(TAG, "Backup not allowed: a restore is pending.")
|
||||
false
|
||||
}
|
||||
|
||||
SignalStore.backup.restoreState == RestoreState.RESTORING_DB -> {
|
||||
ArchiveRestoreProgress.state.activelyRestoring() -> {
|
||||
Log.i(TAG, "Backup not allowed: a restore is in progress.")
|
||||
false
|
||||
}
|
||||
|
||||
@@ -47,6 +47,10 @@ class BackupRestoreMediaJob private constructor(parameters: Parameters) : BaseJo
|
||||
|
||||
override fun onFailure() = Unit
|
||||
|
||||
override fun onAdded() {
|
||||
ArchiveRestoreProgress.onStartMediaRestore()
|
||||
}
|
||||
|
||||
override fun onRun() {
|
||||
if (!SignalStore.account.isRegistered) {
|
||||
Log.e(TAG, "Not registered, cannot restore!")
|
||||
@@ -121,7 +125,7 @@ class BackupRestoreMediaJob private constructor(parameters: Parameters) : BaseJo
|
||||
} while (restoreThumbnailJobs.isNotEmpty() || restoreFullAttachmentJobs.isNotEmpty() || notRestorable.isNotEmpty())
|
||||
|
||||
BackupMediaRestoreService.start(context, context.getString(R.string.BackupStatus__restoring_media))
|
||||
SignalStore.backup.totalRestorableAttachmentSize = SignalDatabase.attachments.getRemainingRestorableAttachmentSize()
|
||||
ArchiveRestoreProgress.onRestoringMedia()
|
||||
|
||||
RestoreAttachmentJob.Queues.INITIAL_RESTORE.forEach { queue ->
|
||||
jobManager.add(CheckRestoreMediaLeftJob(queue))
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.jobs
|
||||
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.backup.v2.ArchiveRestoreProgress
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.jobmanager.Job
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
|
||||
class CancelRestoreMediaJob private constructor(parameters: Parameters) : Job(parameters) {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(CancelRestoreMediaJob::class)
|
||||
const val KEY = "CancelRestoreMediaJob"
|
||||
|
||||
fun enqueue() {
|
||||
AppDependencies.jobManager.add(
|
||||
CancelRestoreMediaJob(parameters = Parameters.Builder().build())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun serialize(): ByteArray? = null
|
||||
|
||||
override fun getFactoryKey(): String = KEY
|
||||
|
||||
override fun run(): Result {
|
||||
SignalStore.backup.userManuallySkippedMediaRestore = true
|
||||
|
||||
ArchiveRestoreProgress.onCancelMediaRestore()
|
||||
|
||||
Log.i(TAG, "Canceling all media restore jobs")
|
||||
RestoreAttachmentJob.Queues.ALL.forEach { AppDependencies.jobManager.cancelAllInQueue(it) }
|
||||
|
||||
Log.i(TAG, "Enqueueing check restore media jobs to cleanup")
|
||||
RestoreAttachmentJob.Queues.ALL.forEach { AppDependencies.jobManager.add(CheckRestoreMediaLeftJob(it)) }
|
||||
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
override fun onFailure() = Unit
|
||||
|
||||
class Factory : Job.Factory<CancelRestoreMediaJob?> {
|
||||
override fun create(parameters: Parameters, serializedData: ByteArray?): CancelRestoreMediaJob {
|
||||
return CancelRestoreMediaJob(parameters = parameters)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,6 @@ package org.thoughtcrime.securesms.jobs
|
||||
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.backup.DeletionState
|
||||
import org.thoughtcrime.securesms.backup.RestoreState
|
||||
import org.thoughtcrime.securesms.backup.v2.ArchiveRestoreProgress
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
@@ -46,21 +45,17 @@ class CheckRestoreMediaLeftJob private constructor(parameters: Parameters) : Job
|
||||
val remainingAttachmentSize = SignalDatabase.attachments.getRemainingRestorableAttachmentSize()
|
||||
|
||||
if (remainingAttachmentSize == 0L) {
|
||||
if (SignalStore.backup.restoreState != RestoreState.NONE) {
|
||||
Log.d(TAG, "Media restore complete: there are no remaining restorable attachments.")
|
||||
SignalStore.backup.totalRestorableAttachmentSize = 0
|
||||
SignalStore.backup.restoreState = RestoreState.NONE
|
||||
ArchiveRestoreProgress.onProcessEnd()
|
||||
BackupMediaRestoreService.stop(context)
|
||||
Log.d(TAG, "Media restore complete: there are no remaining restorable attachments.")
|
||||
ArchiveRestoreProgress.allMediaRestored()
|
||||
BackupMediaRestoreService.stop(context)
|
||||
|
||||
if (SignalStore.backup.deletionState == DeletionState.AWAITING_MEDIA_DOWNLOAD) {
|
||||
SignalStore.backup.deletionState = DeletionState.MEDIA_DOWNLOAD_FINISHED
|
||||
}
|
||||
if (SignalStore.backup.deletionState == DeletionState.AWAITING_MEDIA_DOWNLOAD) {
|
||||
SignalStore.backup.deletionState = DeletionState.MEDIA_DOWNLOAD_FINISHED
|
||||
}
|
||||
|
||||
if (!SignalStore.backup.backsUpMedia) {
|
||||
SignalDatabase.attachments.markQuotesThatNeedReconstruction()
|
||||
AppDependencies.jobManager.add(QuoteThumbnailReconstructionJob())
|
||||
}
|
||||
if (!SignalStore.backup.backsUpMedia) {
|
||||
SignalDatabase.attachments.markQuotesThatNeedReconstruction()
|
||||
AppDependencies.jobManager.add(QuoteThumbnailReconstructionJob())
|
||||
}
|
||||
} else if (runAttempt == 0) {
|
||||
Log.w(TAG, "Still have remaining data to restore, will retry before checking job queues, queue: ${parameters.queue} estimated remaining: $remainingAttachmentSize")
|
||||
|
||||
@@ -149,6 +149,7 @@ public final class JobManagerFactories {
|
||||
put(CallLinkUpdateSendJob.KEY, new CallLinkUpdateSendJob.Factory());
|
||||
put(CallLogEventSendJob.KEY, new CallLogEventSendJob.Factory());
|
||||
put(CallSyncEventJob.KEY, new CallSyncEventJob.Factory());
|
||||
put(CancelRestoreMediaJob.KEY, new CancelRestoreMediaJob.Factory());
|
||||
put(CheckRestoreMediaLeftJob.KEY, new CheckRestoreMediaLeftJob.Factory());
|
||||
put(CheckServiceReachabilityJob.KEY, new CheckServiceReachabilityJob.Factory());
|
||||
put(CleanPreKeysJob.KEY, new CleanPreKeysJob.Factory());
|
||||
|
||||
@@ -12,6 +12,7 @@ import org.signal.core.util.logging.Log
|
||||
import org.signal.libsignal.protocol.InvalidMacException
|
||||
import org.signal.libsignal.protocol.InvalidMessageException
|
||||
import org.thoughtcrime.securesms.attachments.AttachmentId
|
||||
import org.thoughtcrime.securesms.backup.v2.ArchiveRestoreProgress
|
||||
import org.thoughtcrime.securesms.backup.v2.local.ArchiveFileSystem
|
||||
import org.thoughtcrime.securesms.database.AttachmentTable
|
||||
import org.thoughtcrime.securesms.database.AttachmentTable.RestorableAttachment
|
||||
@@ -19,7 +20,6 @@ import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.jobmanager.Job
|
||||
import org.thoughtcrime.securesms.jobs.protos.RestoreLocalAttachmentJobData
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.mms.MmsException
|
||||
import org.whispersystems.signalservice.api.backup.MediaName
|
||||
import org.whispersystems.signalservice.api.crypto.AttachmentCipherInputStream
|
||||
@@ -75,7 +75,7 @@ class RestoreLocalAttachmentJob private constructor(
|
||||
restoreAttachmentJobs.forEach { jobManager.add(it) }
|
||||
} while (restoreAttachmentJobs.isNotEmpty())
|
||||
|
||||
SignalStore.backup.totalRestorableAttachmentSize = SignalDatabase.attachments.getRemainingRestorableAttachmentSize()
|
||||
ArchiveRestoreProgress.onRestoringMedia()
|
||||
|
||||
val checkDoneJobs = (0 until CONCURRENT_QUEUES)
|
||||
.map {
|
||||
|
||||
@@ -6,10 +6,13 @@
|
||||
package org.thoughtcrime.securesms.jobs
|
||||
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.backup.v2.ArchiveRestoreProgress
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.jobmanager.Job
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.service.BackupMediaRestoreService
|
||||
|
||||
/**
|
||||
* Restores any media that was previously optimized and off-loaded into the user's archive. Leverages
|
||||
@@ -61,6 +64,8 @@ class RestoreOptimizedMediaJob private constructor(parameters: Parameters) : Job
|
||||
|
||||
val jobManager = AppDependencies.jobManager
|
||||
|
||||
ArchiveRestoreProgress.onStartMediaRestore()
|
||||
|
||||
restorableAttachments
|
||||
.forEach {
|
||||
val job = RestoreAttachmentJob.forOffloadedRestore(
|
||||
@@ -72,7 +77,8 @@ class RestoreOptimizedMediaJob private constructor(parameters: Parameters) : Job
|
||||
jobManager.add(job)
|
||||
}
|
||||
|
||||
SignalStore.backup.totalRestorableAttachmentSize = SignalDatabase.attachments.getRemainingRestorableAttachmentSize()
|
||||
BackupMediaRestoreService.start(context, context.getString(R.string.BackupStatus__restoring_media))
|
||||
ArchiveRestoreProgress.onRestoringMedia()
|
||||
|
||||
RestoreAttachmentJob.Queues.OFFLOAD_RESTORE.forEach { queue ->
|
||||
AppDependencies.jobManager.add(CheckRestoreMediaLeftJob(queue))
|
||||
|
||||
@@ -116,7 +116,6 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) {
|
||||
|
||||
val deletionStateFlow: Flow<DeletionState> = deletionStateValue.toFlow()
|
||||
|
||||
var restoreState: RestoreState by enumValue(KEY_RESTORE_STATE, RestoreState.NONE, RestoreState.serializer)
|
||||
var optimizeStorage: Boolean by booleanValue(KEY_OPTIMIZE_STORAGE, false)
|
||||
var backupWithCellular: Boolean
|
||||
get() = getBoolean(KEY_BACKUP_OVER_CELLULAR, false)
|
||||
@@ -359,13 +358,8 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) {
|
||||
|
||||
var backupsInitialized: Boolean by booleanValue(KEY_BACKUPS_INITIALIZED, false)
|
||||
|
||||
private val totalRestorableAttachmentSizeValue = longValue(KEY_TOTAL_RESTORABLE_ATTACHMENT_SIZE, 0)
|
||||
var totalRestorableAttachmentSize: Long by totalRestorableAttachmentSizeValue
|
||||
val totalRestorableAttachmentSizeFlow: Flow<Long>
|
||||
get() = totalRestorableAttachmentSizeValue.toFlow()
|
||||
|
||||
val isMediaRestoreInProgress: Boolean
|
||||
get() = totalRestorableAttachmentSize > 0
|
||||
var restoreState: RestoreState by enumValue(KEY_RESTORE_STATE, RestoreState.NONE, RestoreState.serializer)
|
||||
var totalRestorableAttachmentSize: Long by longValue(KEY_TOTAL_RESTORABLE_ATTACHMENT_SIZE, 0)
|
||||
|
||||
/** Store that lets you interact with message ZK credentials. */
|
||||
val messageCredentials = CredentialStore(KEY_MESSAGE_CREDENTIALS, KEY_MESSAGE_CDN_READ_CREDENTIALS, KEY_MESSAGE_CDN_READ_CREDENTIALS_TIMESTAMP)
|
||||
|
||||
@@ -8,6 +8,7 @@ package org.thoughtcrime.securesms.logsubmit
|
||||
import android.content.Context
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.signal.donations.InAppPaymentType
|
||||
import org.thoughtcrime.securesms.backup.v2.ArchiveRestoreProgress
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toFiatMoney
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
@@ -39,7 +40,7 @@ class LogSectionRemoteBackups : LogSection {
|
||||
output.append("Optimize storage: ${SignalStore.backup.optimizeStorage}\n")
|
||||
output.append("Detected subscription state mismatch: ${SignalStore.backup.subscriptionStateMismatchDetected}\n")
|
||||
output.append("Last verified key time: ${SignalStore.backup.lastVerifyKeyTime}\n")
|
||||
output.append("Media restore state: ${SignalStore.backup.restoreState}\n")
|
||||
output.append("Restore state: ${ArchiveRestoreProgress.state}\n")
|
||||
output.append("\n -- Subscription State\n")
|
||||
|
||||
val backupSubscriptionId = InAppPaymentsRepository.getSubscriber(InAppPaymentSubscriberRecord.Type.BACKUP)
|
||||
|
||||
@@ -21,9 +21,7 @@ import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.reactive.asFlow
|
||||
import kotlinx.coroutines.rx3.asObservable
|
||||
import org.thoughtcrime.securesms.backup.RestoreState
|
||||
import org.thoughtcrime.securesms.components.settings.app.notifications.profiles.NotificationProfilesRepository
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.megaphone.Megaphone
|
||||
@@ -64,9 +62,6 @@ class MainNavigationViewModel(
|
||||
private val internalMainNavigationState = MutableStateFlow(MainNavigationState(currentListLocation = initialListLocation))
|
||||
val mainNavigationState: StateFlow<MainNavigationState> = internalMainNavigationState
|
||||
|
||||
private val internalBackupStatus = MutableStateFlow(0L)
|
||||
val backupStatus: StateFlow<Long> = internalBackupStatus
|
||||
|
||||
/**
|
||||
* This is Rx because these are still accessed from Java.
|
||||
*/
|
||||
@@ -89,8 +84,6 @@ class MainNavigationViewModel(
|
||||
performStoreUpdate(MainNavigationRepository.getHasFailedOutgoingStories()) { hasFailedStories, state ->
|
||||
state.copy(storyFailure = hasFailedStories)
|
||||
}
|
||||
|
||||
getRemainingRestoreAttachmentSize()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -219,18 +212,6 @@ class MainNavigationViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
private fun getRemainingRestoreAttachmentSize() {
|
||||
viewModelScope.launch {
|
||||
internalBackupStatus.update {
|
||||
if (SignalStore.backup.restoreState == RestoreState.RESTORING_MEDIA) {
|
||||
SignalDatabase.attachments.getRemainingRestorableAttachmentSize()
|
||||
} else {
|
||||
0L
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T : Any> performStoreUpdate(flow: Flow<T>, fn: (T, MainNavigationState) -> MainNavigationState) {
|
||||
viewModelScope.launch {
|
||||
flow.collectLatest { item ->
|
||||
|
||||
@@ -13,8 +13,7 @@ import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.MainActivity
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.backup.RestoreState
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.backup.v2.ArchiveRestoreProgress
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels
|
||||
import org.thoughtcrime.securesms.notifications.NotificationIds
|
||||
import org.thoughtcrime.securesms.service.BackupMediaRestoreService.Companion.hasTimedOut
|
||||
@@ -176,12 +175,10 @@ class BackupMediaRestoreService : SafeForegroundService() {
|
||||
|
||||
val title: String = startingTitle
|
||||
val downloadedBytes: ByteSize
|
||||
get() {
|
||||
val remainingAttachmentSize = SignalDatabase.attachments.getRemainingRestorableAttachmentSize()
|
||||
return totalBytes - remainingAttachmentSize.bytes
|
||||
}
|
||||
get() = ArchiveRestoreProgress.state.completedRestoredSize
|
||||
|
||||
val totalBytes: ByteSize
|
||||
get() = SignalStore.backup.totalRestorableAttachmentSize.bytes
|
||||
get() = ArchiveRestoreProgress.state.totalRestoreSize
|
||||
|
||||
fun closeFromTimeout() {
|
||||
controllerLock.withLock {
|
||||
|
||||
@@ -7960,6 +7960,8 @@
|
||||
<string name="BackupStatus__free_up_s_of_space_to_download_your_media">Free up %1$s of space to restore your media.</string>
|
||||
<!-- Status title for banner when user is actively restoring media from a backup -->
|
||||
<string name="BackupStatus__restoring_media">Restoring media</string>
|
||||
<!-- Status title for banner when user is canceling restore media from a backup -->
|
||||
<string name="BackupStatus__cancel_restore_media">Canceling media restore</string>
|
||||
<!-- Status title for banner when user has paused restore media from a backup -->
|
||||
<string name="BackupStatus__restore_paused">Media restore paused</string>
|
||||
<!-- Status title for banner when user has completed restoring restore media from a backup -->
|
||||
|
||||
@@ -116,6 +116,23 @@ class ByteSize(val bytes: Long) {
|
||||
return ByteSize(this.inWholeBytes * other)
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "ByteSize(${toUnitString(maxPlaces = 4, spaced = false)})"
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as ByteSize
|
||||
|
||||
return bytes == other.bytes
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return bytes.hashCode()
|
||||
}
|
||||
|
||||
enum class Size(val label: String) {
|
||||
BYTE("B"),
|
||||
KIBIBYTE("KB"),
|
||||
|
||||
Reference in New Issue
Block a user