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.