Show upload progress for the first backup on the chat list.

This commit is contained in:
Greyson Parrelli
2026-01-21 16:58:15 -05:00
committed by Alex Hart
parent 4c43bf2228
commit 2f6baf8743
12 changed files with 577 additions and 30 deletions

View File

@@ -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
)
}
}
}

View File

@@ -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)
)
}

View File

@@ -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")
)
}
}
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}
}

View File

@@ -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()
}
}

View File

@@ -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());
}
})
);

View File

@@ -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

View 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>

View 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>

View File

@@ -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>