mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-14 23:18:43 +00:00
Show upload progress for the first backup on the chat list.
This commit is contained in:
committed by
Alex Hart
parent
4c43bf2228
commit
2f6baf8743
@@ -46,13 +46,11 @@ object ArchiveUploadProgress {
|
||||
|
||||
private val TAG = Log.tag(ArchiveUploadProgress::class)
|
||||
|
||||
private val PROGRESS_NONE = ArchiveUploadProgressState(
|
||||
state = ArchiveUploadProgressState.State.None
|
||||
)
|
||||
|
||||
private val _progress: MutableSharedFlow<Unit> = MutableSharedFlow(replay = 1)
|
||||
|
||||
private var uploadProgress: ArchiveUploadProgressState = SignalStore.backup.archiveUploadState ?: PROGRESS_NONE
|
||||
private var uploadProgress: ArchiveUploadProgressState = SignalStore.backup.archiveUploadState ?: ArchiveUploadProgressState(
|
||||
state = ArchiveUploadProgressState.State.None
|
||||
)
|
||||
|
||||
private val attachmentProgress: MutableMap<AttachmentId, AttachmentProgressDetails> = ConcurrentHashMap()
|
||||
|
||||
@@ -76,7 +74,11 @@ object ArchiveUploadProgress {
|
||||
|
||||
if (!SignalStore.backup.backsUpMedia) {
|
||||
Log.i(TAG, "Doesn't upload media. Done!")
|
||||
return@map PROGRESS_NONE
|
||||
SignalStore.backup.finishedInitialBackup = true
|
||||
return@map uploadProgress.copy(
|
||||
state = ArchiveUploadProgressState.State.None,
|
||||
backupPhase = ArchiveUploadProgressState.BackupPhase.BackupPhaseNone
|
||||
)
|
||||
}
|
||||
|
||||
val pendingMediaUploadBytes = SignalDatabase.attachments.getPendingArchiveUploadBytes() - attachmentProgress.values.sumOf { it.bytesUploaded }
|
||||
@@ -87,7 +89,12 @@ object ArchiveUploadProgress {
|
||||
Log.i(TAG, "We uploaded media as part of the backup. We should enqueue another backup now to ensure that CDN info is properly written.")
|
||||
BackupMessagesJob.enqueue()
|
||||
}
|
||||
return@map PROGRESS_NONE
|
||||
SignalStore.backup.finishedInitialBackup = true
|
||||
return@map uploadProgress.copy(
|
||||
state = ArchiveUploadProgressState.State.None,
|
||||
backupPhase = ArchiveUploadProgressState.BackupPhase.BackupPhaseNone,
|
||||
mediaUploadedBytes = uploadProgress.mediaTotalBytes
|
||||
)
|
||||
}
|
||||
|
||||
// It's possible that new attachments may be pending upload after we start a backup.
|
||||
@@ -96,7 +103,7 @@ object ArchiveUploadProgress {
|
||||
// the progress bar may occasionally be including media that is not actually referenced in the active backup file.
|
||||
val totalMediaUploadBytes = max(uploadProgress.mediaTotalBytes, pendingMediaUploadBytes)
|
||||
|
||||
ArchiveUploadProgressState(
|
||||
uploadProgress.copy(
|
||||
state = ArchiveUploadProgressState.State.UploadMedia,
|
||||
mediaUploadedBytes = totalMediaUploadBytes - pendingMediaUploadBytes,
|
||||
mediaTotalBytes = totalMediaUploadBytes
|
||||
@@ -109,9 +116,13 @@ object ArchiveUploadProgress {
|
||||
.flowOn(Dispatchers.IO)
|
||||
|
||||
val inProgress
|
||||
get() = uploadProgress.state != ArchiveUploadProgressState.State.None
|
||||
get() = uploadProgress.state != ArchiveUploadProgressState.State.None && uploadProgress.state != ArchiveUploadProgressState.State.UserCanceled
|
||||
|
||||
fun begin() {
|
||||
if (!SignalStore.backup.finishedInitialBackup) {
|
||||
SignalStore.backup.uploadBannerVisible = true
|
||||
}
|
||||
|
||||
updateState(overrideCancel = true) {
|
||||
ArchiveUploadProgressState(
|
||||
state = ArchiveUploadProgressState.State.Export
|
||||
@@ -124,6 +135,8 @@ object ArchiveUploadProgress {
|
||||
}
|
||||
|
||||
fun cancel() {
|
||||
SignalStore.backup.uploadBannerVisible = false
|
||||
|
||||
updateState {
|
||||
ArchiveUploadProgressState(
|
||||
state = ArchiveUploadProgressState.State.UserCanceled
|
||||
@@ -213,6 +226,7 @@ object ArchiveUploadProgress {
|
||||
|
||||
fun onMessageBackupFinishedEarly() {
|
||||
resetState()
|
||||
SignalStore.backup.finishedInitialBackup = true
|
||||
}
|
||||
|
||||
fun onMainBackupFileUploadFailure() {
|
||||
@@ -224,7 +238,12 @@ object ArchiveUploadProgress {
|
||||
if (shouldRevertToUploadMedia) {
|
||||
onAttachmentSectionStarted(SignalDatabase.attachments.getPendingArchiveUploadBytes())
|
||||
} else {
|
||||
updateState { PROGRESS_NONE }
|
||||
updateState {
|
||||
it.copy(
|
||||
state = ArchiveUploadProgressState.State.None,
|
||||
backupPhase = ArchiveUploadProgressState.BackupPhase.BackupPhaseNone
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ private const val NONE = -1
|
||||
*/
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun BackupStatusBanner(
|
||||
fun ArchiveRestoreStatusBanner(
|
||||
data: ArchiveRestoreProgressState,
|
||||
onBannerClick: () -> Unit = {},
|
||||
onActionClick: (ArchiveRestoreProgressState) -> Unit = {},
|
||||
@@ -311,46 +311,46 @@ private fun ArchiveRestoreProgressState.actionResource(): Int {
|
||||
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
fun BackupStatusBannerPreview() {
|
||||
private fun ArchiveRestoreStatusBannerPreview() {
|
||||
Previews.Preview {
|
||||
Column {
|
||||
BackupStatusBanner(
|
||||
ArchiveRestoreStatusBanner(
|
||||
data = ArchiveRestoreProgressState(restoreState = RestoreState.RESTORING_MEDIA, restoreStatus = RestoreStatus.RESTORING, remainingRestoreSize = 800.mebiBytes, totalRestoreSize = 1024.mebiBytes)
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
BackupStatusBanner(
|
||||
ArchiveRestoreStatusBanner(
|
||||
data = ArchiveRestoreProgressState(restoreState = RestoreState.CALCULATING_MEDIA, restoreStatus = RestoreStatus.RESTORING, remainingRestoreSize = 1024.mebiBytes, totalRestoreSize = 1024.mebiBytes)
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
BackupStatusBanner(
|
||||
ArchiveRestoreStatusBanner(
|
||||
data = ArchiveRestoreProgressState(restoreState = RestoreState.CANCELING_MEDIA, restoreStatus = RestoreStatus.RESTORING, remainingRestoreSize = 200.mebiBytes, totalRestoreSize = 1024.mebiBytes)
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
BackupStatusBanner(
|
||||
ArchiveRestoreStatusBanner(
|
||||
data = ArchiveRestoreProgressState(restoreState = RestoreState.RESTORING_MEDIA, restoreStatus = RestoreStatus.WAITING_FOR_WIFI, remainingRestoreSize = 800.mebiBytes, totalRestoreSize = 1024.mebiBytes)
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
BackupStatusBanner(
|
||||
ArchiveRestoreStatusBanner(
|
||||
data = ArchiveRestoreProgressState(restoreState = RestoreState.RESTORING_MEDIA, restoreStatus = RestoreStatus.WAITING_FOR_INTERNET, remainingRestoreSize = 800.mebiBytes, totalRestoreSize = 1024.mebiBytes)
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
BackupStatusBanner(
|
||||
ArchiveRestoreStatusBanner(
|
||||
data = ArchiveRestoreProgressState(restoreState = RestoreState.NONE, restoreStatus = RestoreStatus.FINISHED, remainingRestoreSize = 0.mebiBytes, totalRestoreSize = 0.mebiBytes, totalToRestoreThisRun = 1024.mebiBytes)
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
BackupStatusBanner(
|
||||
ArchiveRestoreStatusBanner(
|
||||
data = ArchiveRestoreProgressState(restoreState = RestoreState.RESTORING_MEDIA, restoreStatus = RestoreStatus.NOT_ENOUGH_DISK_SPACE, remainingRestoreSize = 500.mebiBytes, totalRestoreSize = 1024.mebiBytes)
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2.ui.status
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.signal.core.ui.compose.DayNightPreviews
|
||||
import org.signal.core.ui.compose.DropdownMenus
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.BackupsIconColors
|
||||
|
||||
/**
|
||||
* Displays a "heads up" widget containing information about the current
|
||||
* status of the user's backup.
|
||||
*/
|
||||
@OptIn(ExperimentalLayoutApi::class, ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun ArchiveUploadStatusBannerView(
|
||||
state: ArchiveUploadStatusBannerViewState,
|
||||
emitter: (ArchiveUploadStatusBannerViewEvents) -> Unit = {},
|
||||
contentPadding: PaddingValues = PaddingValues(horizontal = 12.dp, vertical = 8.dp)
|
||||
) {
|
||||
val iconRes: Int = when (state) {
|
||||
ArchiveUploadStatusBannerViewState.CreatingBackupFile -> R.drawable.symbol_backup_light
|
||||
is ArchiveUploadStatusBannerViewState.Uploading -> R.drawable.symbol_backup_light
|
||||
is ArchiveUploadStatusBannerViewState.PausedMissingWifi -> R.drawable.symbol_backup_light
|
||||
is ArchiveUploadStatusBannerViewState.PausedNoInternet -> R.drawable.symbol_backup_light
|
||||
is ArchiveUploadStatusBannerViewState.Finished -> R.drawable.symbol_check_circle_24
|
||||
}
|
||||
|
||||
val iconColor: Color = when (state) {
|
||||
ArchiveUploadStatusBannerViewState.CreatingBackupFile -> BackupsIconColors.Normal.foreground
|
||||
is ArchiveUploadStatusBannerViewState.Uploading -> BackupsIconColors.Normal.foreground
|
||||
is ArchiveUploadStatusBannerViewState.PausedMissingWifi -> BackupsIconColors.Warning.foreground
|
||||
is ArchiveUploadStatusBannerViewState.PausedNoInternet -> BackupsIconColors.Warning.foreground
|
||||
is ArchiveUploadStatusBannerViewState.Finished -> BackupsIconColors.Success.foreground
|
||||
}
|
||||
|
||||
val title: String = when (state) {
|
||||
ArchiveUploadStatusBannerViewState.CreatingBackupFile -> stringResource(R.string.BackupStatus__status_creating_backup)
|
||||
is ArchiveUploadStatusBannerViewState.Uploading -> stringResource(R.string.BackupStatus__uploading_backup)
|
||||
is ArchiveUploadStatusBannerViewState.PausedMissingWifi -> stringResource(R.string.BackupStatus__upload_paused)
|
||||
is ArchiveUploadStatusBannerViewState.PausedNoInternet -> stringResource(R.string.BackupStatus__upload_paused)
|
||||
is ArchiveUploadStatusBannerViewState.Finished -> stringResource(R.string.BackupStatus__upload_complete)
|
||||
}
|
||||
|
||||
val status: String? = when (state) {
|
||||
ArchiveUploadStatusBannerViewState.CreatingBackupFile -> null
|
||||
is ArchiveUploadStatusBannerViewState.Uploading -> stringResource(R.string.BackupStatus__status_size_of_size, state.completedSize, state.totalSize)
|
||||
is ArchiveUploadStatusBannerViewState.PausedMissingWifi -> stringResource(R.string.BackupStatus__status_waiting_for_wifi)
|
||||
is ArchiveUploadStatusBannerViewState.PausedNoInternet -> stringResource(R.string.BackupStatus__status_no_internet)
|
||||
is ArchiveUploadStatusBannerViewState.Finished -> state.uploadedSize
|
||||
}
|
||||
|
||||
val showIndeterminateProgress: Boolean = when (state) {
|
||||
ArchiveUploadStatusBannerViewState.CreatingBackupFile -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
val progress: Float? = when (state) {
|
||||
is ArchiveUploadStatusBannerViewState.Uploading -> state.progress
|
||||
else -> null
|
||||
}
|
||||
|
||||
val actionLabelRes: Int? = when (state) {
|
||||
is ArchiveUploadStatusBannerViewState.PausedMissingWifi -> R.string.BackupStatus__resume
|
||||
else -> null
|
||||
}
|
||||
|
||||
val showDismiss: Boolean = when (state) {
|
||||
is ArchiveUploadStatusBannerViewState.Finished -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
val dropdownAvailable: Boolean = when (state) {
|
||||
is ArchiveUploadStatusBannerViewState.Finished -> false
|
||||
else -> true
|
||||
}
|
||||
|
||||
val menuController = remember { DropdownMenus.MenuController() }
|
||||
|
||||
Box(modifier = Modifier.padding(contentPadding)) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.border(1.dp, color = MaterialTheme.colorScheme.outline.copy(alpha = 0.38f), shape = RoundedCornerShape(12.dp))
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.fillMaxWidth()
|
||||
.defaultMinSize(minHeight = 48.dp)
|
||||
.combinedClickable(
|
||||
onClick = { emitter(ArchiveUploadStatusBannerViewEvents.BannerClicked) },
|
||||
onLongClick = { menuController.show() }
|
||||
)
|
||||
.padding(12.dp)
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = iconRes),
|
||||
contentDescription = null,
|
||||
tint = iconColor,
|
||||
modifier = Modifier
|
||||
.padding(start = 4.dp)
|
||||
.size(24.dp)
|
||||
)
|
||||
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
modifier = Modifier
|
||||
.padding(start = 12.dp)
|
||||
.weight(1f)
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier
|
||||
.padding(end = 20.dp)
|
||||
.align(Alignment.CenterVertically)
|
||||
)
|
||||
|
||||
status?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier
|
||||
.padding(end = 12.dp)
|
||||
.align(Alignment.CenterVertically)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (showIndeterminateProgress) {
|
||||
CircularProgressIndicator(
|
||||
strokeWidth = 3.dp,
|
||||
strokeCap = StrokeCap.Round,
|
||||
modifier = Modifier
|
||||
.size(24.dp, 24.dp)
|
||||
)
|
||||
}
|
||||
|
||||
progress?.let {
|
||||
CircularProgressIndicator(
|
||||
progress = { it },
|
||||
strokeWidth = 3.dp,
|
||||
strokeCap = StrokeCap.Round,
|
||||
modifier = Modifier
|
||||
.size(24.dp, 24.dp)
|
||||
)
|
||||
}
|
||||
|
||||
if (showDismiss) {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.symbol_x_24),
|
||||
contentDescription = stringResource(R.string.Material3SearchToolbar__close),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.clickable(
|
||||
interactionSource = interactionSource,
|
||||
indication = ripple(bounded = false),
|
||||
onClick = { emitter(ArchiveUploadStatusBannerViewEvents.HideClicked) }
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (dropdownAvailable) {
|
||||
Box(modifier = Modifier.align(Alignment.BottomEnd)) {
|
||||
DropdownMenus.Menu(
|
||||
controller = menuController,
|
||||
offsetX = 0.dp,
|
||||
offsetY = 10.dp
|
||||
) {
|
||||
DropdownMenus.ItemWithIcon(
|
||||
menuController = menuController,
|
||||
drawableResId = R.drawable.symbol_visible_slash,
|
||||
stringResId = R.string.BackupStatus__hide,
|
||||
onClick = { emitter(ArchiveUploadStatusBannerViewEvents.HideClicked) }
|
||||
)
|
||||
DropdownMenus.ItemWithIcon(
|
||||
menuController = menuController,
|
||||
drawableResId = R.drawable.symbol_x_circle_24,
|
||||
stringResId = R.string.BackupStatus__cancel_upload,
|
||||
onClick = { emitter(ArchiveUploadStatusBannerViewEvents.CancelClicked) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun ArchiveUploadStatusBannerViewPreview() {
|
||||
Previews.Preview {
|
||||
Column {
|
||||
ArchiveUploadStatusBannerView(
|
||||
state = ArchiveUploadStatusBannerViewState.CreatingBackupFile
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
ArchiveUploadStatusBannerView(
|
||||
state = ArchiveUploadStatusBannerViewState.Uploading(
|
||||
completedSize = "224 MB",
|
||||
totalSize = "1 GB",
|
||||
progress = 0.22f
|
||||
)
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
ArchiveUploadStatusBannerView(
|
||||
state = ArchiveUploadStatusBannerViewState.Uploading(
|
||||
completedSize = "0 B",
|
||||
totalSize = "1 GB",
|
||||
progress = 0f
|
||||
)
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
ArchiveUploadStatusBannerView(
|
||||
state = ArchiveUploadStatusBannerViewState.PausedMissingWifi
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
ArchiveUploadStatusBannerView(
|
||||
state = ArchiveUploadStatusBannerViewState.PausedNoInternet
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
ArchiveUploadStatusBannerView(
|
||||
state = ArchiveUploadStatusBannerViewState.Finished(uploadedSize = "1 GB")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2.ui.status
|
||||
|
||||
sealed interface ArchiveUploadStatusBannerViewEvents {
|
||||
data object BannerClicked : ArchiveUploadStatusBannerViewEvents
|
||||
data object CancelClicked : ArchiveUploadStatusBannerViewEvents
|
||||
data object HideClicked : ArchiveUploadStatusBannerViewEvents
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2.ui.status
|
||||
|
||||
/**
|
||||
* Renderable state for [ArchiveUploadStatusBannerView].
|
||||
* Each state carries only the data it needs; rendering logic lives in the composable.
|
||||
*/
|
||||
sealed interface ArchiveUploadStatusBannerViewState {
|
||||
/** Creating the intiial backup file. */
|
||||
data object CreatingBackupFile : ArchiveUploadStatusBannerViewState
|
||||
|
||||
/** Actively uploading media with progress. */
|
||||
data class Uploading(
|
||||
val completedSize: String,
|
||||
val totalSize: String,
|
||||
val progress: Float
|
||||
) : ArchiveUploadStatusBannerViewState
|
||||
|
||||
/** Restore paused because Wi-Fi is required. */
|
||||
data object PausedMissingWifi : ArchiveUploadStatusBannerViewState
|
||||
|
||||
/** Restore paused because there is no internet connection. */
|
||||
data object PausedNoInternet : ArchiveUploadStatusBannerViewState
|
||||
|
||||
/** Restore completed successfully. */
|
||||
data class Finished(
|
||||
val uploadedSize: String
|
||||
) : ArchiveUploadStatusBannerViewState
|
||||
}
|
||||
@@ -13,11 +13,11 @@ 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.ArchiveRestoreStatusBanner
|
||||
import org.thoughtcrime.securesms.banner.Banner
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class MediaRestoreProgressBanner(private val listener: RestoreProgressBannerListener = EmptyListener) : Banner<ArchiveRestoreProgressState>() {
|
||||
class ArchiveRestoreStatusBanner(private val listener: RestoreProgressBannerListener) : Banner<ArchiveRestoreProgressState>() {
|
||||
|
||||
override val enabled: Boolean
|
||||
get() = ArchiveRestoreProgress.state.let { it.restoreState.isMediaRestoreOperation || it.restoreStatus == RestoreStatus.FINISHED }
|
||||
@@ -32,7 +32,7 @@ class MediaRestoreProgressBanner(private val listener: RestoreProgressBannerList
|
||||
|
||||
@Composable
|
||||
override fun DisplayBanner(model: ArchiveRestoreProgressState, contentPadding: PaddingValues) {
|
||||
BackupStatusBanner(
|
||||
ArchiveRestoreStatusBanner(
|
||||
data = model,
|
||||
onBannerClick = listener::onBannerClick,
|
||||
onActionClick = listener::onActionClick,
|
||||
@@ -48,10 +48,4 @@ class MediaRestoreProgressBanner(private val listener: RestoreProgressBannerList
|
||||
fun onActionClick(data: ArchiveRestoreProgressState)
|
||||
fun onDismissComplete()
|
||||
}
|
||||
|
||||
private object EmptyListener : RestoreProgressBannerListener {
|
||||
override fun onBannerClick() = Unit
|
||||
override fun onActionClick(data: ArchiveRestoreProgressState) = Unit
|
||||
override fun onDismissComplete() = Unit
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.banner.banners
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.runtime.Composable
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.signal.core.util.bytes
|
||||
import org.thoughtcrime.securesms.backup.ArchiveUploadProgress
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.status.ArchiveUploadStatusBannerView
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.status.ArchiveUploadStatusBannerViewEvents
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.status.ArchiveUploadStatusBannerViewState
|
||||
import org.thoughtcrime.securesms.banner.Banner
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.keyvalue.protos.ArchiveUploadProgressState
|
||||
import org.thoughtcrime.securesms.util.NetworkUtil
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class ArchiveUploadStatusBanner(private val listener: UploadProgressBannerListener) : Banner<ArchiveUploadStatusBannerViewState>() {
|
||||
|
||||
override val enabled: Boolean
|
||||
get() = SignalStore.backup.uploadBannerVisible
|
||||
|
||||
override val dataFlow: Flow<ArchiveUploadStatusBannerViewState> by lazy {
|
||||
ArchiveUploadProgress
|
||||
.progress
|
||||
.map {
|
||||
val hasMobileData = NetworkUtil.isConnectedMobile(AppDependencies.application)
|
||||
val hasWifi = NetworkUtil.isConnectedWifi(AppDependencies.application)
|
||||
val canUploadOnCellular = SignalStore.backup.backupWithCellular
|
||||
|
||||
if (hasWifi || (hasMobileData && canUploadOnCellular)) {
|
||||
when (it.state) {
|
||||
ArchiveUploadProgressState.State.None -> ArchiveUploadStatusBannerViewState.Finished(it.completedSize.bytes.toUnitString(maxPlaces = 1))
|
||||
ArchiveUploadProgressState.State.Export -> ArchiveUploadStatusBannerViewState.CreatingBackupFile
|
||||
ArchiveUploadProgressState.State.UploadBackupFile,
|
||||
ArchiveUploadProgressState.State.UploadMedia -> {
|
||||
if (NetworkConstraint.isMet(AppDependencies.application)) {
|
||||
ArchiveUploadStatusBannerViewState.Uploading(
|
||||
completedSize = it.completedSize.bytes.toUnitString(maxPlaces = 1),
|
||||
totalSize = it.totalSize.bytes.toUnitString(maxPlaces = 1),
|
||||
progress = it.completedSize / it.totalSize.toFloat()
|
||||
)
|
||||
} else {
|
||||
ArchiveUploadStatusBannerViewState.PausedNoInternet
|
||||
}
|
||||
}
|
||||
ArchiveUploadProgressState.State.UserCanceled -> ArchiveUploadStatusBannerViewState.CreatingBackupFile
|
||||
}
|
||||
} else if (hasMobileData) {
|
||||
when (it.state) {
|
||||
ArchiveUploadProgressState.State.None,
|
||||
ArchiveUploadProgressState.State.Export,
|
||||
ArchiveUploadProgressState.State.UploadBackupFile,
|
||||
ArchiveUploadProgressState.State.UploadMedia -> {
|
||||
ArchiveUploadStatusBannerViewState.PausedMissingWifi
|
||||
}
|
||||
ArchiveUploadProgressState.State.UserCanceled -> ArchiveUploadStatusBannerViewState.CreatingBackupFile
|
||||
}
|
||||
} else {
|
||||
when (it.state) {
|
||||
ArchiveUploadProgressState.State.None,
|
||||
ArchiveUploadProgressState.State.Export,
|
||||
ArchiveUploadProgressState.State.UploadBackupFile,
|
||||
ArchiveUploadProgressState.State.UploadMedia -> {
|
||||
ArchiveUploadStatusBannerViewState.PausedNoInternet
|
||||
}
|
||||
ArchiveUploadProgressState.State.UserCanceled -> ArchiveUploadStatusBannerViewState.CreatingBackupFile
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun DisplayBanner(model: ArchiveUploadStatusBannerViewState, contentPadding: PaddingValues) {
|
||||
ArchiveUploadStatusBannerView(
|
||||
state = model,
|
||||
emitter = { event ->
|
||||
when (event) {
|
||||
ArchiveUploadStatusBannerViewEvents.BannerClicked -> {
|
||||
listener.onBannerClick()
|
||||
}
|
||||
ArchiveUploadStatusBannerViewEvents.CancelClicked -> {
|
||||
listener.onCancelClicked()
|
||||
}
|
||||
ArchiveUploadStatusBannerViewEvents.HideClicked -> {
|
||||
SignalStore.backup.uploadBannerVisible = false
|
||||
listener.onHidden()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private val ArchiveUploadProgressState.completedSize get() = this.mediaUploadedBytes + this.backupFileUploadedBytes
|
||||
private val ArchiveUploadProgressState.totalSize get() = this.mediaTotalBytes + this.backupFileTotalBytes
|
||||
|
||||
interface UploadProgressBannerListener {
|
||||
fun onBannerClick()
|
||||
fun onCancelClicked()
|
||||
fun onHidden()
|
||||
}
|
||||
}
|
||||
@@ -76,6 +76,7 @@ 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.ArchiveUploadProgress;
|
||||
import org.thoughtcrime.securesms.backup.RestoreState;
|
||||
import org.thoughtcrime.securesms.backup.v2.ArchiveRestoreProgress;
|
||||
import org.thoughtcrime.securesms.backup.v2.ArchiveRestoreProgressState;
|
||||
@@ -87,12 +88,13 @@ import org.thoughtcrime.securesms.badges.self.expired.ExpiredOneTimeBadgeBottomS
|
||||
import org.thoughtcrime.securesms.badges.self.expired.MonthlyDonationCanceledBottomSheetDialogFragment;
|
||||
import org.thoughtcrime.securesms.banner.Banner;
|
||||
import org.thoughtcrime.securesms.banner.BannerManager;
|
||||
import org.thoughtcrime.securesms.banner.banners.ArchiveUploadStatusBanner;
|
||||
import org.thoughtcrime.securesms.banner.banners.CdsPermanentErrorBanner;
|
||||
import org.thoughtcrime.securesms.banner.banners.CdsTemporaryErrorBanner;
|
||||
import org.thoughtcrime.securesms.banner.banners.DeprecatedBuildBanner;
|
||||
import org.thoughtcrime.securesms.banner.banners.DeprecatedSdkBanner;
|
||||
import org.thoughtcrime.securesms.banner.banners.DozeBanner;
|
||||
import org.thoughtcrime.securesms.banner.banners.MediaRestoreProgressBanner;
|
||||
import org.thoughtcrime.securesms.banner.banners.ArchiveRestoreStatusBanner;
|
||||
import org.thoughtcrime.securesms.banner.banners.OutdatedBuildBanner;
|
||||
import org.thoughtcrime.securesms.banner.banners.ServiceOutageBanner;
|
||||
import org.thoughtcrime.securesms.banner.banners.UnauthorizedBanner;
|
||||
@@ -745,7 +747,7 @@ public class ConversationListFragment extends MainFragment implements Conversati
|
||||
}
|
||||
return Unit.INSTANCE;
|
||||
}),
|
||||
new MediaRestoreProgressBanner(new MediaRestoreProgressBanner.RestoreProgressBannerListener() {
|
||||
new ArchiveRestoreStatusBanner(new ArchiveRestoreStatusBanner.RestoreProgressBannerListener() {
|
||||
@Override
|
||||
public void onBannerClick() {
|
||||
startActivity(AppSettingsActivity.backupsSettings(requireContext()));
|
||||
@@ -774,6 +776,30 @@ public class ConversationListFragment extends MainFragment implements Conversati
|
||||
public void onDismissComplete() {
|
||||
bannerManager.updateContent(bannerView.get());
|
||||
}
|
||||
}),
|
||||
new ArchiveUploadStatusBanner(new ArchiveUploadStatusBanner.UploadProgressBannerListener() {
|
||||
@Override
|
||||
public void onBannerClick() {
|
||||
startActivity(AppSettingsActivity.remoteBackups(requireContext()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCancelClicked() {
|
||||
new MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.CancelBackupDialog_title)
|
||||
.setMessage(R.string.CancelBackupDialog_body)
|
||||
.setNegativeButton(R.string.CancelBackupDialog_continue_action, null)
|
||||
.setPositiveButton(R.string.CancelBackupDialog_cancel_action, (d, w) -> {
|
||||
ArchiveUploadProgress.INSTANCE.cancel();
|
||||
bannerManager.updateContent(bannerView.get());
|
||||
})
|
||||
.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHidden() {
|
||||
bannerManager.updateContent(bannerView.get());
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) {
|
||||
private const val KEY_LAST_CHECK_IN_MILLIS = "backup.lastCheckInMilliseconds"
|
||||
private const val KEY_LAST_CHECK_IN_SNOOZE_MILLIS = "backup.lastCheckInSnoozeMilliseconds"
|
||||
private const val KEY_FIRST_APP_VERSION = "backup.firstAppVersion"
|
||||
private const val KEY_FINISHED_INITIAL_BACKUP = "backup.finishedInitialBackup"
|
||||
|
||||
private const val KEY_NEXT_BACKUP_TIME = "backup.nextBackupTime"
|
||||
private const val KEY_LAST_BACKUP_TIME = "backup.lastBackupTime"
|
||||
@@ -103,6 +104,8 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) {
|
||||
private const val KEY_NEW_LOCAL_BACKUPS_DIRECTORY = "backup.new_local_backups_directory"
|
||||
private const val KEY_NEW_LOCAL_BACKUPS_LAST_BACKUP_TIME = "backup.new_local_backups_last_backup_time"
|
||||
|
||||
private const val KEY_UPLOAD_BANNER_VISIBLE = "backup.upload_banner_visible"
|
||||
|
||||
private val cachedCdnCredentialsExpiresIn: Duration = 12.hours
|
||||
|
||||
private val lock = ReentrantLock()
|
||||
@@ -158,6 +161,16 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the user has completed their initial backup. This if enabling backups for the first time or going from free -> paid tier.
|
||||
* It is initialized to 'true' to account for pre-existing backup users.
|
||||
* Users who newly-enable backups will set this to 'false' via the aforementioned mechanism.
|
||||
*/
|
||||
var finishedInitialBackup: Boolean by booleanValue(KEY_FINISHED_INITIAL_BACKUP, true)
|
||||
|
||||
/** Whether or not the user chose to hide the uplaod banner that appears on the chat list. */
|
||||
var uploadBannerVisible: Boolean by booleanValue(KEY_UPLOAD_BANNER_VISIBLE, false)
|
||||
|
||||
private val _lastBackupTimeFlow: Lazy<MutableStateFlow<Long>> = lazy { MutableStateFlow(lastBackupTime) }
|
||||
val lastBackupTimeFlow by lazy { _lastBackupTimeFlow.value }
|
||||
|
||||
@@ -268,6 +281,13 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) {
|
||||
clearNotEnoughRemoteStorageSpace()
|
||||
clearMessageBackupFailureSheetWatermark()
|
||||
backupCreationError = null
|
||||
|
||||
if (storedValue == null) {
|
||||
Log.i(TAG, "Enabling backups. Resetting 'finished initial backup' state.")
|
||||
} else if (value == MessageBackupTier.PAID) {
|
||||
Log.i(TAG, "Moving to PAID backups. Resetting 'finished initial backup' state.")
|
||||
finishedInitialBackup = false
|
||||
}
|
||||
}
|
||||
|
||||
deletionState = DeletionState.NONE
|
||||
|
||||
12
app/src/main/res/drawable/symbol_visible.xml
Normal file
12
app/src/main/res/drawable/symbol_visible.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M16.5 12c0 2.49-2.01 4.5-4.5 4.5S7.5 14.49 7.5 12 9.51 7.5 12 7.5s4.5 2.01 4.5 4.5Zm-3 0c0-0.83-0.67-1.5-1.5-1.5s-1.5 0.67-1.5 1.5 0.67 1.5 1.5 1.5 1.5-0.67 1.5-1.5Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M12 4.13c-3.2 0-5.72 1.37-7.54 2.9C2.65 8.56 1.5 10.3 1 11.11c-0.33 0.55-0.33 1.23 0 1.78 0.5 0.82 1.65 2.55 3.46 4.08 1.82 1.53 4.35 2.9 7.54 2.9 3.2 0 5.72-1.37 7.54-2.9 1.81-1.53 2.97-3.26 3.46-4.08 0.33-0.55 0.33-1.23 0-1.78-0.5-0.82-1.65-2.55-3.46-4.08-1.82-1.53-4.35-2.9-7.54-2.9Zm-6.4 11.5c-1.62-1.36-2.66-2.9-3.1-3.63 0.44-0.73 1.48-2.27 3.1-3.63 1.6-1.36 3.74-2.5 6.4-2.5s4.8 1.14 6.4 2.5c1.62 1.36 2.66 2.9 3.1 3.63-0.44 0.73-1.48 2.27-3.1 3.63-1.6 1.36-3.74 2.5-6.4 2.5s-4.8-1.14-6.4-2.5Z"/>
|
||||
</vector>
|
||||
21
app/src/main/res/drawable/symbol_visible_slash.xml
Normal file
21
app/src/main/res/drawable/symbol_visible_slash.xml
Normal file
@@ -0,0 +1,21 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M4.12 2.88c-0.34-0.34-0.9-0.34-1.24 0-0.34 0.34-0.34 0.9 0 1.24l16.97 16.97c0.34 0.34 0.9 0.34 1.24 0 0.34-0.34 0.34-0.9 0-1.24L4.12 2.88Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M1 11.1c0.45-0.74 1.47-2.27 3.04-3.7l1.24 1.24C3.85 9.93 2.92 11.32 2.51 12c0.43 0.73 1.47 2.27 3.08 3.63 1.6 1.36 3.75 2.5 6.41 2.5 0.87 0 1.68-0.13 2.44-0.34l1.38 1.4c-1.14 0.42-2.42 0.68-3.82 0.68-3.2 0-5.72-1.37-7.54-2.9C2.65 15.44 1.5 13.7 1 12.89c-0.33-0.55-0.33-1.23 0-1.78Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M7.5 12c0-0.35 0.04-0.7 0.12-1.02l5.4 5.4C12.7 16.46 12.35 16.5 12 16.5c-2.49 0-4.5-2.01-4.5-4.5Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M10.98 7.62l5.4 5.4c0.08-0.32 0.12-0.67 0.12-1.02 0-2.49-2.01-4.5-4.5-4.5-0.35 0-0.7 0.04-1.02 0.12Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M21.5 12c-0.42 0.68-1.35 2.07-2.78 3.36l1.24 1.24c1.57-1.43 2.59-2.96 3.04-3.7 0.33-0.56 0.33-1.24 0-1.8-0.5-0.8-1.65-2.54-3.46-4.07-1.82-1.53-4.35-2.9-7.54-2.9-1.4 0-2.68 0.26-3.82 0.69L9.56 6.2C10.32 6 11.13 5.88 12 5.88c2.66 0 4.8 1.13 6.4 2.49 1.62 1.36 2.66 2.9 3.1 3.63Z"/>
|
||||
</vector>
|
||||
@@ -8080,6 +8080,15 @@
|
||||
<!-- Status action label for resume restore from a paused state -->
|
||||
<string name="BackupStatus__resume">Resume</string>
|
||||
|
||||
<!-- Status subtitle for banner when we are creating a backup file -->
|
||||
<string name="BackupStatus__status_creating_backup">Creating backup…</string>
|
||||
<!-- Status title for banner when user is actively uploading backup data -->
|
||||
<string name="BackupStatus__uploading_backup">Uploading backup</string>
|
||||
<!-- Status title for banner when user is temporarily unable to upload a backup -->
|
||||
<string name="BackupStatus__upload_paused">Backup paused</string>
|
||||
<!-- Status title for banner when user has finished creating a backup -->
|
||||
<string name="BackupStatus__upload_complete">Backup complete</string>
|
||||
|
||||
<!-- Status subtitle for banner when restoring media pauses for Wi-Fi -->
|
||||
<string name="BackupStatus__status_waiting_for_wifi">Waiting for Wi-Fi…</string>
|
||||
<!-- Status subtitle for banner when restoring media pauses for internet in general -->
|
||||
@@ -8088,6 +8097,10 @@
|
||||
<string name="BackupStatus__status_device_has_low_battery">Device has low battery</string>
|
||||
<!-- Status subtitle for banner when restoring media. Placeholders are size already restored and total size to restore. e.g., 4.5MB of 100MB -->
|
||||
<string name="BackupStatus__status_size_of_size">%1$s of %2$s</string>
|
||||
<!-- Label for a context menu item that, when pressed, will hide a banner so it will no longer be shown. -->
|
||||
<string name="BackupStatus__hide">Hide</string>
|
||||
<!-- Label for a context menu item that, when pressed, will cancel the ongoing backup. -->
|
||||
<string name="BackupStatus__cancel_upload">Cancel backup</string>
|
||||
|
||||
<!-- BackupStatusRow -->
|
||||
<!-- Content description for x icon at the end of the linear progress indicator -->
|
||||
@@ -8130,6 +8143,15 @@
|
||||
<!-- Dialog message to prompt resuming media restore over cellular -->
|
||||
<string name="ResumeRestoreCellular_resume_using_cellular_message">Restoring your media using cellular data may result in data charges. You can connect to a Wi-Fi network to automatically resume.</string>
|
||||
|
||||
<!-- Title of a dialog box prompting to confirm whether the user truly wants to cancel their backup. -->
|
||||
<string name="CancelBackupDialog_title">Cancel backup?</string>
|
||||
<!-- Body of a dialog box prompting to confirm whether the user truly wants to cancel their backup. -->
|
||||
<string name="CancelBackupDialog_body">Canceling your backup will not delete your backup. You can resume your backup at any time from Backup Settings.</string>
|
||||
<!-- Button on a dialog box that confirming backup cancellation. When pressed, this button will confirm canceling the currently-running backup (future backups are unaffected). -->
|
||||
<string name="CancelBackupDialog_cancel_action">Cancel backup</string>
|
||||
<!-- Button on a dialog box that confirming backup cancellation. When pressed, this button will continue the backup rather than cancel it. -->
|
||||
<string name="CancelBackupDialog_continue_action">Continue backup</string>
|
||||
|
||||
<!-- BackupsTypeSettingsFragment -->
|
||||
<!-- Displayed as the user\'s payment method as a label in a preference row -->
|
||||
<string name="BackupsTypeSettingsFragment__google_play">Google Play</string>
|
||||
|
||||
Reference in New Issue
Block a user