diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/status/BackupStatusBanner.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/status/BackupStatusBanner.kt
index 58c8486cc7..d955da0ef8 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/status/BackupStatusBanner.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/status/BackupStatusBanner.kt
@@ -55,7 +55,7 @@ private const val NONE = -1
*/
@OptIn(ExperimentalLayoutApi::class)
@Composable
-fun BackupStatus(
+fun BackupStatusBanner(
data: BackupStatusData,
onSkipClick: () -> Unit = {},
onDismissClick: () -> Unit = {},
@@ -150,13 +150,13 @@ fun BackupStatus(
fun BackupStatusBannerPreview() {
Previews.Preview {
Column {
- BackupStatus(
+ BackupStatusBanner(
data = BackupStatusData.RestoringMedia(5755000.bytes, 1253.mebiBytes)
)
HorizontalDivider()
- BackupStatus(
+ BackupStatusBanner(
data = BackupStatusData.RestoringMedia(
bytesDownloaded = 55000.bytes,
bytesTotal = 1253.mebiBytes,
@@ -166,7 +166,7 @@ fun BackupStatusBannerPreview() {
HorizontalDivider()
- BackupStatus(
+ BackupStatusBanner(
data = BackupStatusData.RestoringMedia(
bytesDownloaded = 55000.bytes,
bytesTotal = 1253.mebiBytes,
@@ -176,7 +176,7 @@ fun BackupStatusBannerPreview() {
HorizontalDivider()
- BackupStatus(
+ BackupStatusBanner(
data = BackupStatusData.RestoringMedia(
bytesDownloaded = 55000.bytes,
bytesTotal = 1253.mebiBytes,
@@ -186,13 +186,13 @@ fun BackupStatusBannerPreview() {
HorizontalDivider()
- BackupStatus(
+ BackupStatusBanner(
data = BackupStatusData.NotEnoughFreeSpace(40900.kibiBytes)
)
HorizontalDivider()
- BackupStatus(
+ BackupStatusBanner(
data = BackupStatusData.CouldNotCompleteBackup
)
}
@@ -241,7 +241,7 @@ sealed interface BackupStatusData {
class NotEnoughFreeSpace(
requiredSpace: ByteSize
) : BackupStatusData {
- private val requiredSpace = requiredSpace.toUnitString(maxPlaces = 2)
+ val requiredSpace = requiredSpace.toUnitString(maxPlaces = 2)
override val iconRes: Int = R.drawable.symbol_backup_error_24
diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/status/BackupStatusRow.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/status/BackupStatusRow.kt
new file mode 100644
index 0000000000..31a51a5aea
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/status/BackupStatusRow.kt
@@ -0,0 +1,240 @@
+/*
+ * Copyright 2024 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.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.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.Placeholder
+import androidx.compose.ui.text.PlaceholderVerticalAlign
+import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import org.signal.core.ui.Previews
+import org.signal.core.ui.Rows
+import org.signal.core.ui.SignalPreview
+import org.signal.core.util.ByteSize
+import org.thoughtcrime.securesms.R
+import kotlin.math.roundToInt
+
+/**
+ * Backup status displayable as a row on a settings page.
+ */
+@Composable
+fun BackupStatusRow(
+ backupStatusData: BackupStatusData,
+ onSkipClick: () -> Unit = {},
+ onCancelClick: () -> Unit = {}
+) {
+ Column {
+ if (backupStatusData !is BackupStatusData.CouldNotCompleteBackup) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.padding(horizontal = dimensionResource(R.dimen.core_ui__gutter))
+ ) {
+ LinearProgressIndicator(
+ color = progressColor(backupStatusData),
+ progress = { backupStatusData.progress },
+ modifier = Modifier.weight(1f)
+ )
+
+ IconButton(
+ onClick = onCancelClick
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.symbol_x_24),
+ contentDescription = stringResource(R.string.BackupStatusRow__cancel_download)
+ )
+ }
+ }
+ }
+
+ when (backupStatusData) {
+ is BackupStatusData.RestoringMedia -> {
+ Text(
+ text = getRestoringMediaString(backupStatusData),
+ modifier = Modifier.padding(horizontal = dimensionResource(R.dimen.core_ui__gutter))
+ )
+ }
+
+ is BackupStatusData.NotEnoughFreeSpace -> {
+ Text(
+ text = stringResource(
+ R.string.BackupStatusRow__not_enough_space,
+ backupStatusData.requiredSpace,
+ "%d".format((backupStatusData.progress * 100).roundToInt())
+ ),
+ modifier = Modifier.padding(horizontal = dimensionResource(R.dimen.core_ui__gutter))
+ )
+
+ Rows.TextRow(
+ text = stringResource(R.string.BackupStatusRow__skip_download),
+ onClick = onSkipClick
+ )
+ }
+
+ BackupStatusData.CouldNotCompleteBackup -> {
+ val inlineContentMap = mapOf(
+ "yellow_bullet" to InlineTextContent(
+ Placeholder(12.sp, 12.sp, PlaceholderVerticalAlign.TextCenter)
+ ) {
+ Box(
+ modifier = Modifier
+ .size(12.dp)
+ .background(color = backupStatusData.iconColors.foreground, shape = CircleShape)
+ )
+ }
+ )
+
+ Text(
+ text = buildAnnotatedString {
+ appendInlineContent("yellow_bullet")
+ append(" ")
+ append(stringResource(R.string.BackupStatusRow__your_last_backup))
+ },
+ inlineContent = inlineContentMap,
+ modifier = Modifier.padding(horizontal = dimensionResource(R.dimen.core_ui__gutter))
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun getRestoringMediaString(backupStatusData: BackupStatusData.RestoringMedia): String {
+ return when (backupStatusData.restoreStatus) {
+ BackupStatusData.RestoreStatus.NORMAL -> {
+ stringResource(
+ R.string.BackupStatusRow__downloading_s_of_s_s,
+ backupStatusData.bytesDownloaded.toUnitString(2),
+ backupStatusData.bytesTotal.toUnitString(2),
+ "%d".format((backupStatusData.progress * 100).roundToInt())
+ )
+ }
+ BackupStatusData.RestoreStatus.LOW_BATTERY -> stringResource(R.string.BackupStatus__status_device_has_low_battery)
+ BackupStatusData.RestoreStatus.WAITING_FOR_INTERNET -> stringResource(R.string.BackupStatus__status_no_internet)
+ BackupStatusData.RestoreStatus.WAITING_FOR_WIFI -> stringResource(R.string.BackupStatus__status_waiting_for_wifi)
+ BackupStatusData.RestoreStatus.FINISHED -> stringResource(R.string.BackupStatus__restore_complete)
+ }
+}
+
+@Composable
+private fun progressColor(backupStatusData: BackupStatusData): Color {
+ return when (backupStatusData) {
+ is BackupStatusData.RestoringMedia -> MaterialTheme.colorScheme.primary
+ else -> backupStatusData.iconColors.foreground
+ }
+}
+
+@SignalPreview
+@Composable
+fun BackupStatusRowNormalPreview() {
+ Previews.Preview {
+ BackupStatusRow(
+ backupStatusData = BackupStatusData.RestoringMedia(
+ bytesTotal = ByteSize(100),
+ bytesDownloaded = ByteSize(50),
+ restoreStatus = BackupStatusData.RestoreStatus.NORMAL
+ )
+ )
+ }
+}
+
+@SignalPreview
+@Composable
+fun BackupStatusRowWaitingForWifiPreview() {
+ Previews.Preview {
+ BackupStatusRow(
+ backupStatusData = BackupStatusData.RestoringMedia(
+ bytesTotal = ByteSize(100),
+ bytesDownloaded = ByteSize(50),
+ restoreStatus = BackupStatusData.RestoreStatus.WAITING_FOR_WIFI
+ )
+ )
+ }
+}
+
+@SignalPreview
+@Composable
+fun BackupStatusRowWaitingForInternetPreview() {
+ Previews.Preview {
+ BackupStatusRow(
+ backupStatusData = BackupStatusData.RestoringMedia(
+ bytesTotal = ByteSize(100),
+ bytesDownloaded = ByteSize(50),
+ restoreStatus = BackupStatusData.RestoreStatus.WAITING_FOR_INTERNET
+ )
+ )
+ }
+}
+
+@SignalPreview
+@Composable
+fun BackupStatusRowLowBatteryPreview() {
+ Previews.Preview {
+ BackupStatusRow(
+ backupStatusData = BackupStatusData.RestoringMedia(
+ bytesTotal = ByteSize(100),
+ bytesDownloaded = ByteSize(50),
+ restoreStatus = BackupStatusData.RestoreStatus.LOW_BATTERY
+ )
+ )
+ }
+}
+
+@SignalPreview
+@Composable
+fun BackupStatusRowFinishedPreview() {
+ Previews.Preview {
+ BackupStatusRow(
+ backupStatusData = BackupStatusData.RestoringMedia(
+ bytesTotal = ByteSize(100),
+ bytesDownloaded = ByteSize(50),
+ restoreStatus = BackupStatusData.RestoreStatus.FINISHED
+ )
+ )
+ }
+}
+
+@SignalPreview
+@Composable
+fun BackupStatusRowNotEnoughFreeSpacePreview() {
+ Previews.Preview {
+ BackupStatusRow(
+ backupStatusData = BackupStatusData.NotEnoughFreeSpace(
+ requiredSpace = ByteSize(50)
+ )
+ )
+ }
+}
+
+@SignalPreview
+@Composable
+fun BackupStatusRowCouldNotCompleteBackupPreview() {
+ Previews.Preview {
+ BackupStatusRow(
+ backupStatusData = BackupStatusData.CouldNotCompleteBackup
+ )
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/banner/banners/MediaRestoreProgressBanner.kt b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/MediaRestoreProgressBanner.kt
index 88124836ca..aad635e31c 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/banner/banners/MediaRestoreProgressBanner.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/MediaRestoreProgressBanner.kt
@@ -23,7 +23,7 @@ import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import org.signal.core.util.bytes
import org.signal.core.util.throttleLatest
-import org.thoughtcrime.securesms.backup.v2.ui.status.BackupStatus
+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
@@ -71,7 +71,7 @@ class MediaRestoreProgressBanner(private val listener: RestoreProgressBannerList
@Composable
override fun DisplayBanner(model: BackupStatusData, contentPadding: PaddingValues) {
- BackupStatus(
+ BackupStatusBanner(
data = model,
onSkipClick = listener::onSkip,
onDismissClick = listener::onDismissComplete
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index ba2ce78cdc..8045c8cc0a 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -7443,6 +7443,17 @@
%1$s of %2$s
+
+
+ Cancel download
+
+ Downloading: %1$s of %2$s (%3$s%%)
+
+ Not enough space to download your Backup. To continue free up %1$s of space.
+
+ Skip download
+
+ Your last backup couldn\'t be completed. Make sure your phone is connected to Wi-F and tap "Back up now" to try again.