mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-21 02:08:40 +00:00
Update restore progress banner UI/UX and job behavior.
This commit is contained in:
committed by
Greyson Parrelli
parent
320d51707d
commit
93609106b0
@@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
@@ -35,7 +36,6 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.signal.core.ui.BottomSheets
|
||||
import org.signal.core.ui.Buttons
|
||||
import org.signal.core.ui.Icons
|
||||
import org.signal.core.ui.Previews
|
||||
import org.signal.core.ui.SignalPreview
|
||||
import org.thoughtcrime.securesms.R
|
||||
@@ -144,10 +144,10 @@ private fun BackupAlertSheetContent(
|
||||
Spacer(modifier = Modifier.size(26.dp))
|
||||
|
||||
val iconColors = rememberBackupsIconColors(backupAlert = backupAlert)
|
||||
Icons.BrushedForeground(
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.symbol_backup_light), // TODO [message-backups] final asset
|
||||
contentDescription = null,
|
||||
foregroundBrush = iconColors.foreground,
|
||||
tint = iconColors.foreground,
|
||||
modifier = Modifier
|
||||
.size(88.dp)
|
||||
.background(color = iconColors.background, shape = CircleShape)
|
||||
|
||||
@@ -7,39 +7,32 @@ package org.thoughtcrime.securesms.backup.v2.ui
|
||||
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
|
||||
sealed interface BackupsIconColors {
|
||||
@get:Composable
|
||||
val foreground: Brush
|
||||
val foreground: Color
|
||||
|
||||
@get:Composable
|
||||
val background: Color
|
||||
|
||||
data object Normal : BackupsIconColors {
|
||||
override val foreground: Brush
|
||||
@Composable get() = remember {
|
||||
Brush.linearGradient(
|
||||
colors = listOf(Color(0xFF316ED0), Color(0xFF558BE2)),
|
||||
start = Offset(x = 0f, y = Float.POSITIVE_INFINITY),
|
||||
end = Offset(x = Float.POSITIVE_INFINITY, y = 0f)
|
||||
)
|
||||
}
|
||||
|
||||
override val foreground: Color @Composable get() = MaterialTheme.colorScheme.onSurface
|
||||
override val background: Color @Composable get() = MaterialTheme.colorScheme.primaryContainer
|
||||
}
|
||||
|
||||
object Warning : BackupsIconColors {
|
||||
override val foreground: Brush @Composable get() = SolidColor(Color(0xFFC86600))
|
||||
data object Success : BackupsIconColors {
|
||||
override val foreground: Color @Composable get() = MaterialTheme.colorScheme.primary
|
||||
override val background: Color @Composable get() = MaterialTheme.colorScheme.primaryContainer
|
||||
}
|
||||
|
||||
data object Warning : BackupsIconColors {
|
||||
override val foreground: Color @Composable get() = Color(0xFFFF9500)
|
||||
override val background: Color @Composable get() = Color(0xFFF9E4B6)
|
||||
}
|
||||
|
||||
object Error : BackupsIconColors {
|
||||
override val foreground: Brush @Composable get() = SolidColor(MaterialTheme.colorScheme.error)
|
||||
data object Error : BackupsIconColors {
|
||||
override val foreground: Color @Composable get() = MaterialTheme.colorScheme.error
|
||||
override val background: Color @Composable get() = Color(0xFFFFD9D9)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
@@ -29,7 +30,6 @@ import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.setFragmentResult
|
||||
import org.signal.core.ui.BottomSheets
|
||||
import org.signal.core.ui.Buttons
|
||||
import org.signal.core.ui.Icons
|
||||
import org.signal.core.ui.Previews
|
||||
import org.signal.core.ui.SignalPreview
|
||||
import org.thoughtcrime.securesms.R
|
||||
@@ -90,9 +90,9 @@ private fun CreateBackupBottomSheetContent(
|
||||
) {
|
||||
BottomSheets.Handle()
|
||||
|
||||
Icons.BrushedForeground(
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.symbol_backup_light),
|
||||
foregroundBrush = BackupsIconColors.Normal.foreground,
|
||||
tint = BackupsIconColors.Normal.foreground,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(top = 18.dp, bottom = 11.dp)
|
||||
|
||||
@@ -7,31 +7,41 @@ package org.thoughtcrime.securesms.backup.v2.ui.status
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
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.shape.CircleShape
|
||||
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.LinearProgressIndicator
|
||||
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.graphics.Brush
|
||||
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.Buttons
|
||||
import org.signal.core.ui.Icons
|
||||
import org.signal.core.ui.Previews
|
||||
import org.signal.core.ui.SignalPreview
|
||||
import org.signal.core.util.ByteSize
|
||||
import org.signal.core.util.bytes
|
||||
import org.signal.core.util.kibiBytes
|
||||
import org.signal.core.util.mebiBytes
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.BackupsIconColors
|
||||
import kotlin.math.max
|
||||
@@ -43,10 +53,12 @@ private const val NONE = -1
|
||||
* Displays a "heads up" widget containing information about the current
|
||||
* status of the user's backup.
|
||||
*/
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun BackupStatus(
|
||||
data: BackupStatusData,
|
||||
onActionClick: () -> Unit = {},
|
||||
onSkipClick: () -> Unit = {},
|
||||
onDismissClick: () -> Unit = {},
|
||||
contentPadding: PaddingValues = PaddingValues(horizontal = 12.dp, vertical = 8.dp)
|
||||
) {
|
||||
Row(
|
||||
@@ -55,56 +67,81 @@ fun BackupStatus(
|
||||
.padding(contentPadding)
|
||||
.border(1.dp, color = MaterialTheme.colorScheme.outline.copy(alpha = 0.38f), shape = RoundedCornerShape(12.dp))
|
||||
.fillMaxWidth()
|
||||
.padding(14.dp)
|
||||
.defaultMinSize(minHeight = 48.dp)
|
||||
.padding(12.dp)
|
||||
) {
|
||||
val foreground: Brush = data.iconColors.foreground
|
||||
Icons.BrushedForeground(
|
||||
Icon(
|
||||
painter = painterResource(id = data.iconRes),
|
||||
contentDescription = null,
|
||||
foregroundBrush = foreground,
|
||||
tint = data.iconColors.foreground,
|
||||
modifier = Modifier
|
||||
.background(color = data.iconColors.background, shape = CircleShape)
|
||||
.padding(8.dp)
|
||||
.padding(start = 4.dp)
|
||||
.size(24.dp)
|
||||
)
|
||||
|
||||
Column(
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
modifier = Modifier
|
||||
.padding(start = 12.dp)
|
||||
.weight(1f)
|
||||
) {
|
||||
Text(
|
||||
text = data.title,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier
|
||||
.padding(end = 20.dp)
|
||||
.align(Alignment.CenterVertically)
|
||||
)
|
||||
|
||||
if (data.progress >= 0f) {
|
||||
LinearProgressIndicator(
|
||||
progress = { data.progress },
|
||||
strokeCap = StrokeCap.Round,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 6.dp)
|
||||
)
|
||||
}
|
||||
|
||||
if (data.statusRes != NONE) {
|
||||
data.status?.let { status ->
|
||||
Text(
|
||||
text = stringResource(id = data.statusRes),
|
||||
text = status,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier
|
||||
.padding(end = 12.dp)
|
||||
.align(Alignment.CenterVertically)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (data.progress >= 0f) {
|
||||
CircularProgressIndicator(
|
||||
progress = { data.progress },
|
||||
strokeWidth = 3.dp,
|
||||
strokeCap = StrokeCap.Round,
|
||||
modifier = Modifier
|
||||
.size(24.dp, 24.dp)
|
||||
)
|
||||
}
|
||||
|
||||
if (data.actionRes != NONE) {
|
||||
Buttons.Small(
|
||||
onClick = onActionClick,
|
||||
onClick = onSkipClick,
|
||||
modifier = Modifier.padding(start = 8.dp)
|
||||
) {
|
||||
Text(text = stringResource(id = data.actionRes))
|
||||
}
|
||||
}
|
||||
|
||||
if (data.showDismissAction) {
|
||||
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 = onDismissClick
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,29 +150,37 @@ fun BackupStatus(
|
||||
fun BackupStatusPreview() {
|
||||
Previews.Preview {
|
||||
Column {
|
||||
BackupStatus(
|
||||
data = BackupStatusData.RestoringMedia(5755000.bytes, 1253.mebiBytes)
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
BackupStatus(
|
||||
data = BackupStatusData.RestoringMedia(
|
||||
bytesDownloaded = 55000.bytes,
|
||||
bytesTotal = 1253.mebiBytes,
|
||||
restoreStatus = BackupStatusData.RestoreStatus.FINISHED
|
||||
)
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
BackupStatus(
|
||||
data = BackupStatusData.NotEnoughFreeSpace(40900.kibiBytes)
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
BackupStatus(
|
||||
data = BackupStatusData.CouldNotCompleteBackup
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
BackupStatus(
|
||||
data = BackupStatusData.NotEnoughFreeSpace("12 GB")
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
BackupStatus(
|
||||
data = BackupStatusData.RestoringMedia(50, 100)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sealed interface describing status data to display in BackupStatus widget.
|
||||
*
|
||||
* TODO [message-requests] - Finalize assets and text
|
||||
*/
|
||||
sealed interface BackupStatusData {
|
||||
|
||||
@@ -150,16 +195,18 @@ sealed interface BackupStatusData {
|
||||
@get:StringRes
|
||||
val actionRes: Int get() = NONE
|
||||
|
||||
@get:StringRes
|
||||
val statusRes: Int get() = NONE
|
||||
@get:Composable
|
||||
val status: String? get() = null
|
||||
|
||||
val progress: Float get() = NONE.toFloat()
|
||||
|
||||
val showDismissAction: Boolean get() = false
|
||||
|
||||
/**
|
||||
* Generic failure
|
||||
*/
|
||||
data object CouldNotCompleteBackup : BackupStatusData {
|
||||
override val iconRes: Int = R.drawable.symbol_backup_light
|
||||
override val iconRes: Int = R.drawable.symbol_backup_error_24
|
||||
|
||||
override val title: String
|
||||
@Composable
|
||||
@@ -172,9 +219,11 @@ sealed interface BackupStatusData {
|
||||
* User does not have enough space on their device to complete backup restoration
|
||||
*/
|
||||
class NotEnoughFreeSpace(
|
||||
private val requiredSpace: String
|
||||
requiredSpace: ByteSize
|
||||
) : BackupStatusData {
|
||||
override val iconRes: Int = R.drawable.symbol_backup_light
|
||||
private val requiredSpace = requiredSpace.toUnitString(maxPlaces = 2)
|
||||
|
||||
override val iconRes: Int = R.drawable.symbol_backup_error_24
|
||||
|
||||
override val title: String
|
||||
@Composable
|
||||
@@ -188,44 +237,51 @@ sealed interface BackupStatusData {
|
||||
* Restoring media, finished, and paused states.
|
||||
*/
|
||||
data class RestoringMedia(
|
||||
val bytesDownloaded: Long,
|
||||
val bytesTotal: Long,
|
||||
val status: Status = Status.NONE
|
||||
val bytesDownloaded: ByteSize = 0.bytes,
|
||||
val bytesTotal: ByteSize = 0.bytes,
|
||||
val restoreStatus: RestoreStatus = RestoreStatus.NORMAL
|
||||
) : BackupStatusData {
|
||||
override val iconRes: Int = R.drawable.symbol_backup_light
|
||||
override val iconColors: BackupsIconColors = BackupsIconColors.Normal
|
||||
override val iconColors: BackupsIconColors = if (restoreStatus == RestoreStatus.FINISHED) BackupsIconColors.Success else BackupsIconColors.Normal
|
||||
override val showDismissAction: Boolean = restoreStatus == RestoreStatus.FINISHED
|
||||
|
||||
override val title: String
|
||||
@Composable get() = stringResource(
|
||||
when (status) {
|
||||
Status.NONE -> R.string.default_error_msg
|
||||
Status.LOW_BATTERY -> R.string.default_error_msg
|
||||
Status.WAITING_FOR_INTERNET -> R.string.default_error_msg
|
||||
Status.WAITING_FOR_WIFI -> R.string.default_error_msg
|
||||
Status.FINISHED -> R.string.default_error_msg
|
||||
when (restoreStatus) {
|
||||
RestoreStatus.NORMAL -> R.string.BackupStatus__restoring_media
|
||||
RestoreStatus.LOW_BATTERY -> R.string.BackupStatus__restore_paused
|
||||
RestoreStatus.WAITING_FOR_INTERNET -> R.string.BackupStatus__restore_paused
|
||||
RestoreStatus.WAITING_FOR_WIFI -> R.string.BackupStatus__restore_paused
|
||||
RestoreStatus.FINISHED -> R.string.BackupStatus__restore_complete
|
||||
}
|
||||
)
|
||||
|
||||
override val statusRes: Int = when (status) {
|
||||
Status.NONE -> NONE
|
||||
Status.LOW_BATTERY -> R.string.default_error_msg
|
||||
Status.WAITING_FOR_INTERNET -> R.string.default_error_msg
|
||||
Status.WAITING_FOR_WIFI -> R.string.default_error_msg
|
||||
Status.FINISHED -> R.string.default_error_msg
|
||||
}
|
||||
override val status: String
|
||||
@Composable get() = when (restoreStatus) {
|
||||
RestoreStatus.NORMAL -> stringResource(
|
||||
R.string.BackupStatus__status_size_of_size,
|
||||
bytesDownloaded.toUnitString(maxPlaces = 2),
|
||||
bytesTotal.toUnitString(maxPlaces = 2)
|
||||
)
|
||||
|
||||
override val progress: Float = if (bytesTotal > 0) {
|
||||
min(1f, max(0f, bytesDownloaded.toFloat() / bytesTotal))
|
||||
RestoreStatus.LOW_BATTERY -> stringResource(R.string.BackupStatus__status_device_has_low_battery)
|
||||
RestoreStatus.WAITING_FOR_INTERNET -> stringResource(R.string.BackupStatus__status_no_internet)
|
||||
RestoreStatus.WAITING_FOR_WIFI -> stringResource(R.string.BackupStatus__status_waiting_for_wifi)
|
||||
RestoreStatus.FINISHED -> bytesTotal.toUnitString()
|
||||
}
|
||||
|
||||
override val progress: Float = if (bytesTotal.bytes > 0 && restoreStatus == RestoreStatus.NORMAL) {
|
||||
min(1f, max(0f, bytesDownloaded.bytes.toFloat() / bytesTotal.bytes.toFloat()))
|
||||
} else {
|
||||
0f
|
||||
NONE.toFloat()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes the status of an in-progress media download session.
|
||||
*/
|
||||
enum class Status {
|
||||
NONE,
|
||||
enum class RestoreStatus {
|
||||
NORMAL,
|
||||
LOW_BATTERY,
|
||||
WAITING_FOR_INTERNET,
|
||||
WAITING_FOR_WIFI,
|
||||
|
||||
@@ -323,7 +323,7 @@ private fun getFeatures(messageBackupsType: MessageBackupsType): List<MessageBac
|
||||
is MessageBackupsType.Paid -> {
|
||||
val photoCount = messageBackupsType.storageAllowanceBytes / ByteUnit.MEGABYTES.toBytes(2)
|
||||
val photoCountThousands = photoCount / 1000
|
||||
val (count, size) = messageBackupsType.storageAllowanceBytes.bytes.getLargestNonZeroValue()
|
||||
val sizeUnitString = messageBackupsType.storageAllowanceBytes.bytes.toUnitString(spaced = false)
|
||||
|
||||
persistentListOf(
|
||||
MessageBackupsTypeFeature(
|
||||
@@ -338,7 +338,7 @@ private fun getFeatures(messageBackupsType: MessageBackupsType): List<MessageBac
|
||||
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
|
||||
label = stringResource(
|
||||
id = R.string.MessageBackupsTypeSelectionScreen__s_of_storage_s_photos,
|
||||
"${count}${size.label}",
|
||||
sizeUnitString,
|
||||
"~${photoCountThousands}K"
|
||||
)
|
||||
),
|
||||
|
||||
@@ -5,15 +5,23 @@
|
||||
|
||||
package org.thoughtcrime.securesms.banner.banners
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.net.ConnectivityManager
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.runtime.Composable
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.signal.core.util.bytes
|
||||
import org.signal.core.util.throttleLatest
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.status.BackupStatus
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.status.BackupStatusData
|
||||
@@ -21,47 +29,102 @@ import org.thoughtcrime.securesms.banner.Banner
|
||||
import org.thoughtcrime.securesms.database.DatabaseObserver
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.BatteryNotLowConstraint
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.WifiConstraint
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.util.safeUnregisterReceiver
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
class MediaRestoreProgressBanner : Banner<BackupStatusData>() {
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class MediaRestoreProgressBanner(private val listener: RestoreProgressBannerListener) : Banner<BackupStatusData>() {
|
||||
|
||||
private var totalRestoredSize: Long = 0
|
||||
|
||||
override val enabled: Boolean
|
||||
get() = SignalStore.backup.isRestoreInProgress
|
||||
get() = SignalStore.backup.isRestoreInProgress || totalRestoredSize > 0
|
||||
|
||||
override val dataFlow: Flow<BackupStatusData>
|
||||
get() {
|
||||
if (!SignalStore.backup.isRestoreInProgress) {
|
||||
return flowOf(BackupStatusData.RestoringMedia(0, 0))
|
||||
}
|
||||
override val dataFlow: Flow<BackupStatusData> by lazy {
|
||||
SignalStore
|
||||
.backup
|
||||
.totalRestorableAttachmentSizeFlow
|
||||
.flatMapLatest { size ->
|
||||
when {
|
||||
size > 0 -> {
|
||||
totalRestoredSize = size
|
||||
getActiveRestoreFlow()
|
||||
}
|
||||
|
||||
val dbNotificationFlow = callbackFlow {
|
||||
val queryObserver = DatabaseObserver.Observer {
|
||||
trySend(Unit)
|
||||
}
|
||||
totalRestoredSize > 0 -> {
|
||||
flowOf(
|
||||
BackupStatusData.RestoringMedia(
|
||||
bytesTotal = totalRestoredSize.bytes.also { totalRestoredSize = 0 },
|
||||
restoreStatus = BackupStatusData.RestoreStatus.FINISHED
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
queryObserver.onChanged()
|
||||
AppDependencies.databaseObserver.registerAttachmentUpdatedObserver(queryObserver)
|
||||
|
||||
awaitClose {
|
||||
AppDependencies.databaseObserver.unregisterObserver(queryObserver)
|
||||
else -> flowOf(BackupStatusData.RestoringMedia())
|
||||
}
|
||||
}
|
||||
|
||||
return dbNotificationFlow
|
||||
.throttleLatest(1.seconds)
|
||||
.map {
|
||||
val totalRestoreSize = SignalStore.backup.totalRestorableAttachmentSize
|
||||
val remainingAttachmentSize = SignalDatabase.attachments.getRemainingRestorableAttachmentSize()
|
||||
val completedBytes = totalRestoreSize - remainingAttachmentSize
|
||||
|
||||
BackupStatusData.RestoringMedia(completedBytes, totalRestoreSize)
|
||||
}
|
||||
.flowOn(Dispatchers.IO)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun DisplayBanner(model: BackupStatusData, contentPadding: PaddingValues) {
|
||||
BackupStatus(data = model)
|
||||
BackupStatus(
|
||||
data = model,
|
||||
onSkipClick = listener::onSkip,
|
||||
onDismissClick = listener::onDismissComplete
|
||||
)
|
||||
}
|
||||
|
||||
private fun getActiveRestoreFlow(): Flow<BackupStatusData.RestoringMedia> {
|
||||
val flow: Flow<Unit> = callbackFlow {
|
||||
val onChange = { trySend(Unit) }
|
||||
|
||||
val observer = DatabaseObserver.Observer {
|
||||
onChange()
|
||||
}
|
||||
|
||||
val receiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
onChange()
|
||||
}
|
||||
}
|
||||
|
||||
onChange()
|
||||
|
||||
AppDependencies.databaseObserver.registerAttachmentUpdatedObserver(observer)
|
||||
AppDependencies.application.registerReceiver(receiver, IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION))
|
||||
AppDependencies.application.registerReceiver(receiver, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
|
||||
|
||||
awaitClose {
|
||||
AppDependencies.databaseObserver.unregisterObserver(observer)
|
||||
AppDependencies.application.safeUnregisterReceiver(receiver)
|
||||
}
|
||||
}
|
||||
|
||||
return flow
|
||||
.throttleLatest(1.seconds)
|
||||
.map {
|
||||
when {
|
||||
!WifiConstraint.isMet(AppDependencies.application) -> BackupStatusData.RestoringMedia(restoreStatus = BackupStatusData.RestoreStatus.WAITING_FOR_WIFI)
|
||||
!NetworkConstraint.isMet(AppDependencies.application) -> BackupStatusData.RestoringMedia(restoreStatus = BackupStatusData.RestoreStatus.WAITING_FOR_INTERNET)
|
||||
!BatteryNotLowConstraint.isMet() -> BackupStatusData.RestoringMedia(restoreStatus = BackupStatusData.RestoreStatus.LOW_BATTERY)
|
||||
else -> {
|
||||
val totalRestoreSize = SignalStore.backup.totalRestorableAttachmentSize
|
||||
val remainingAttachmentSize = SignalDatabase.attachments.getRemainingRestorableAttachmentSize()
|
||||
val completedBytes = totalRestoreSize - remainingAttachmentSize
|
||||
|
||||
BackupStatusData.RestoringMedia(completedBytes.bytes, totalRestoreSize.bytes)
|
||||
}
|
||||
}
|
||||
}
|
||||
.flowOn(Dispatchers.IO)
|
||||
}
|
||||
|
||||
interface RestoreProgressBannerListener {
|
||||
fun onSkip()
|
||||
fun onDismissComplete()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
@@ -29,7 +30,6 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.fragment.app.viewModels
|
||||
import org.signal.core.ui.BottomSheets
|
||||
import org.signal.core.ui.Buttons
|
||||
import org.signal.core.ui.Icons
|
||||
import org.signal.core.ui.Previews
|
||||
import org.signal.core.ui.SignalPreview
|
||||
import org.thoughtcrime.securesms.R
|
||||
@@ -89,10 +89,10 @@ private fun UpgradeToEnableOptimizedStorageSheetContent(
|
||||
) {
|
||||
BottomSheets.Handle()
|
||||
|
||||
Icons.BrushedForeground(
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.symbol_backup_light),
|
||||
contentDescription = null,
|
||||
foregroundBrush = BackupsIconColors.Normal.foreground,
|
||||
tint = BackupsIconColors.Normal.foreground,
|
||||
modifier = Modifier
|
||||
.padding(top = 8.dp, bottom = 12.dp)
|
||||
.size(88.dp)
|
||||
|
||||
@@ -868,7 +868,17 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
}
|
||||
return Unit.INSTANCE;
|
||||
}),
|
||||
new MediaRestoreProgressBanner()
|
||||
new MediaRestoreProgressBanner(new MediaRestoreProgressBanner.RestoreProgressBannerListener() {
|
||||
@Override
|
||||
public void onSkip() {
|
||||
// TODO [backups] add skip restore ability
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDismissComplete() {
|
||||
bannerManager.updateContent(bannerView.get());
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
this.bannerManager = new BannerManager(bannerRepositories);
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package org.thoughtcrime.securesms.jobmanager.impl
|
||||
|
||||
import android.app.job.JobInfo
|
||||
import androidx.annotation.RequiresApi
|
||||
import org.thoughtcrime.securesms.jobmanager.Constraint
|
||||
|
||||
/**
|
||||
* Job constraint for determining whether or not the device battery is not low.
|
||||
*/
|
||||
class BatteryNotLowConstraint private constructor() : Constraint {
|
||||
companion object {
|
||||
const val KEY: String = "BatteryNotLowConstraint"
|
||||
|
||||
fun isMet(): Boolean {
|
||||
return ChargingAndBatteryIsNotLowConstraintObserver.isCharging() || ChargingAndBatteryIsNotLowConstraintObserver.isBatteryNotLow()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getFactoryKey(): String = KEY
|
||||
|
||||
override fun isMet(): Boolean {
|
||||
return Companion.isMet()
|
||||
}
|
||||
|
||||
@RequiresApi(26)
|
||||
override fun applyToJobInfo(jobInfoBuilder: JobInfo.Builder) {
|
||||
jobInfoBuilder.setRequiresBatteryNotLow(true)
|
||||
}
|
||||
|
||||
override fun getJobSchedulerKeyPart(): String? {
|
||||
return "BATTERY_NOT_LOW"
|
||||
}
|
||||
|
||||
class Factory : Constraint.Factory<BatteryNotLowConstraint?> {
|
||||
override fun create(): BatteryNotLowConstraint {
|
||||
return BatteryNotLowConstraint()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package org.thoughtcrime.securesms.jobmanager.impl;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.os.BatteryManager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.jobmanager.ConstraintObserver;
|
||||
|
||||
/**
|
||||
* Observes the charging state and low battery state of the device and notifies the JobManager system when appropriate.
|
||||
*/
|
||||
public class ChargingAndBatteryIsNotLowConstraintObserver implements ConstraintObserver {
|
||||
|
||||
private static final String REASON = Log.tag(ChargingAndBatteryIsNotLowConstraintObserver.class);
|
||||
private static final int STATUS_BATTERY = 0;
|
||||
private static final int LOW_BATTERY_LEVEL = 20;
|
||||
|
||||
private final Application application;
|
||||
|
||||
private static volatile boolean charging;
|
||||
private static volatile boolean batteryNotLow;
|
||||
|
||||
public ChargingAndBatteryIsNotLowConstraintObserver(@NonNull Application application) {
|
||||
this.application = application;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void register(@NonNull Notifier notifier) {
|
||||
Intent intent = application.registerReceiver(new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
boolean wasCharging = charging;
|
||||
boolean wasBatteryNotLow = batteryNotLow;
|
||||
|
||||
charging = isCharging(intent);
|
||||
batteryNotLow = isBatteryNotLow(intent);
|
||||
|
||||
if ((charging && !wasCharging) || (batteryNotLow && !wasBatteryNotLow)) {
|
||||
notifier.onConstraintMet(REASON);
|
||||
}
|
||||
}
|
||||
}, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
|
||||
|
||||
charging = isCharging(intent);
|
||||
}
|
||||
|
||||
public static boolean isCharging() {
|
||||
return charging;
|
||||
}
|
||||
|
||||
public static boolean isBatteryNotLow() {
|
||||
return batteryNotLow;
|
||||
}
|
||||
|
||||
private static boolean isCharging(@Nullable Intent intent) {
|
||||
if (intent == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int status = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, STATUS_BATTERY);
|
||||
return status != STATUS_BATTERY;
|
||||
}
|
||||
|
||||
private static boolean isBatteryNotLow(@Nullable Intent intent) {
|
||||
if (intent == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
|
||||
int scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
|
||||
|
||||
if (level <= 0 || scale <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ((int) Math.floor(level * 100 / (double) scale)) > LOW_BATTERY_LEVEL;
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ public class ChargingConstraint implements Constraint {
|
||||
|
||||
@Override
|
||||
public boolean isMet() {
|
||||
return ChargingConstraintObserver.isCharging();
|
||||
return ChargingAndBatteryIsNotLowConstraintObserver.isCharging();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobmanager.impl;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.os.BatteryManager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.jobmanager.ConstraintObserver;
|
||||
|
||||
/**
|
||||
* Observes the charging state of the device and notifies the JobManager system when appropriate.
|
||||
*/
|
||||
public class ChargingConstraintObserver implements ConstraintObserver {
|
||||
|
||||
private static final String REASON = Log.tag(ChargingConstraintObserver.class);
|
||||
private static final int STATUS_BATTERY = 0;
|
||||
|
||||
private final Application application;
|
||||
|
||||
private static volatile boolean charging;
|
||||
|
||||
public ChargingConstraintObserver(@NonNull Application application) {
|
||||
this.application = application;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void register(@NonNull Notifier notifier) {
|
||||
Intent intent = application.registerReceiver(new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
boolean wasCharging = charging;
|
||||
|
||||
charging = isCharging(intent);
|
||||
|
||||
if (charging && !wasCharging) {
|
||||
notifier.onConstraintMet(REASON);
|
||||
}
|
||||
}
|
||||
}, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
|
||||
|
||||
charging = isCharging(intent);
|
||||
}
|
||||
|
||||
public static boolean isCharging() {
|
||||
return charging;
|
||||
}
|
||||
|
||||
private static boolean isCharging(@Nullable Intent intent) {
|
||||
if (intent == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int status = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, STATUS_BATTERY);
|
||||
return status != STATUS_BATTERY;
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ package org.thoughtcrime.securesms.jobmanager.impl
|
||||
|
||||
import android.app.Application
|
||||
import android.app.job.JobInfo
|
||||
import android.content.Context
|
||||
import org.thoughtcrime.securesms.jobmanager.Constraint
|
||||
import org.thoughtcrime.securesms.util.NetworkUtil
|
||||
|
||||
@@ -17,10 +18,14 @@ class WifiConstraint(private val application: Application) : Constraint {
|
||||
|
||||
companion object {
|
||||
const val KEY = "WifiConstraint"
|
||||
|
||||
fun isMet(context: Context): Boolean {
|
||||
return NetworkUtil.isConnectedWifi(context)
|
||||
}
|
||||
}
|
||||
|
||||
override fun isMet(): Boolean {
|
||||
return NetworkUtil.isConnectedWifi(application)
|
||||
return isMet(application)
|
||||
}
|
||||
|
||||
override fun getFactoryKey(): String = KEY
|
||||
|
||||
@@ -10,11 +10,12 @@ import org.thoughtcrime.securesms.jobmanager.ConstraintObserver;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobMigration;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.AutoDownloadEmojiConstraint;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.BatteryNotLowConstraint;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.CellServiceConstraintObserver;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.ChangeNumberConstraint;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.ChangeNumberConstraintObserver;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.ChargingConstraint;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.ChargingConstraintObserver;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.ChargingAndBatteryIsNotLowConstraintObserver;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.DataRestoreConstraint;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.DataRestoreConstraintObserver;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.DecryptionsDrainedConstraint;
|
||||
@@ -364,6 +365,7 @@ public final class JobManagerFactories {
|
||||
public static Map<String, Constraint.Factory> getConstraintFactories(@NonNull Application application) {
|
||||
return new HashMap<String, Constraint.Factory>() {{
|
||||
put(AutoDownloadEmojiConstraint.KEY, new AutoDownloadEmojiConstraint.Factory(application));
|
||||
put(BatteryNotLowConstraint.KEY, new BatteryNotLowConstraint.Factory());
|
||||
put(ChangeNumberConstraint.KEY, new ChangeNumberConstraint.Factory());
|
||||
put(ChargingConstraint.KEY, new ChargingConstraint.Factory());
|
||||
put(DataRestoreConstraint.KEY, new DataRestoreConstraint.Factory());
|
||||
@@ -379,7 +381,7 @@ public final class JobManagerFactories {
|
||||
|
||||
public static List<ConstraintObserver> getConstraintObservers(@NonNull Application application) {
|
||||
return Arrays.asList(CellServiceConstraintObserver.getInstance(application),
|
||||
new ChargingConstraintObserver(application),
|
||||
new ChargingAndBatteryIsNotLowConstraintObserver(application),
|
||||
new NetworkConstraintObserver(application),
|
||||
new SqlCipherMigrationConstraintObserver(),
|
||||
new DecryptionsDrainedConstraintObserver(),
|
||||
|
||||
@@ -19,7 +19,8 @@ import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.events.PartProgressEvent
|
||||
import org.thoughtcrime.securesms.jobmanager.Job
|
||||
import org.thoughtcrime.securesms.jobmanager.JobLogger.format
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.BatteryNotLowConstraint
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.WifiConstraint
|
||||
import org.thoughtcrime.securesms.jobs.protos.RestoreAttachmentJobData
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.mms.MmsException
|
||||
@@ -107,9 +108,9 @@ class RestoreAttachmentJob private constructor(
|
||||
private constructor(messageId: Long, attachmentId: AttachmentId, manual: Boolean, queue: String) : this(
|
||||
Parameters.Builder()
|
||||
.setQueue(queue)
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.addConstraint(WifiConstraint.KEY)
|
||||
.addConstraint(BatteryNotLowConstraint.KEY)
|
||||
.setLifespan(TimeUnit.DAYS.toMillis(30))
|
||||
.setMaxAttempts(3)
|
||||
.build(),
|
||||
messageId,
|
||||
attachmentId,
|
||||
@@ -129,7 +130,7 @@ class RestoreAttachmentJob private constructor(
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
public override fun onRun() {
|
||||
override fun onRun() {
|
||||
doWork()
|
||||
|
||||
if (!SignalDatabase.messages.isStory(messageId)) {
|
||||
@@ -170,7 +171,7 @@ class RestoreAttachmentJob private constructor(
|
||||
} else {
|
||||
Log.w(TAG, format(this, "onFailure() messageId: $messageId attachmentId: $attachmentId"))
|
||||
|
||||
markFailed(messageId, attachmentId)
|
||||
markFailed(attachmentId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,7 +256,7 @@ class RestoreAttachmentJob private constructor(
|
||||
}
|
||||
} catch (e: InvalidAttachmentException) {
|
||||
Log.w(TAG, "Experienced exception while trying to download an attachment.", e)
|
||||
markFailed(messageId, attachmentId)
|
||||
markFailed(attachmentId)
|
||||
} catch (e: NonSuccessfulResponseCodeException) {
|
||||
if (SignalStore.backup.backsUpMedia) {
|
||||
if (e.code == 404 && !forceTransitTier && attachment.remoteLocation?.isNotBlank() == true) {
|
||||
@@ -269,30 +270,30 @@ class RestoreAttachmentJob private constructor(
|
||||
}
|
||||
|
||||
Log.w(TAG, "Experienced exception while trying to download an attachment.", e)
|
||||
markFailed(messageId, attachmentId)
|
||||
markFailed(attachmentId)
|
||||
} catch (e: MmsException) {
|
||||
Log.w(TAG, "Experienced exception while trying to download an attachment.", e)
|
||||
markFailed(messageId, attachmentId)
|
||||
markFailed(attachmentId)
|
||||
} catch (e: MissingConfigurationException) {
|
||||
Log.w(TAG, "Experienced exception while trying to download an attachment.", e)
|
||||
markFailed(messageId, attachmentId)
|
||||
markFailed(attachmentId)
|
||||
} catch (e: InvalidMessageException) {
|
||||
Log.w(TAG, "Experienced an InvalidMessageException while trying to download an attachment.", e)
|
||||
if (e.cause is InvalidMacException) {
|
||||
Log.w(TAG, "Detected an invalid mac. Treating as a permanent failure.")
|
||||
markPermanentlyFailed(messageId, attachmentId)
|
||||
markPermanentlyFailed(attachmentId)
|
||||
} else {
|
||||
markFailed(messageId, attachmentId)
|
||||
markFailed(attachmentId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun markFailed(messageId: Long, attachmentId: AttachmentId) {
|
||||
SignalDatabase.attachments.setTransferProgressFailed(attachmentId, messageId)
|
||||
private fun markFailed(attachmentId: AttachmentId) {
|
||||
SignalDatabase.attachments.setRestoreTransferState(attachmentId, AttachmentTable.TRANSFER_PROGRESS_FAILED)
|
||||
}
|
||||
|
||||
private fun markPermanentlyFailed(messageId: Long, attachmentId: AttachmentId) {
|
||||
SignalDatabase.attachments.setTransferProgressPermanentFailure(attachmentId, messageId)
|
||||
private fun markPermanentlyFailed(attachmentId: AttachmentId) {
|
||||
SignalDatabase.attachments.setRestoreTransferState(attachmentId, AttachmentTable.TRANSFER_PROGRESS_PERMANENT_FAILURE)
|
||||
}
|
||||
|
||||
class Factory : Job.Factory<RestoreAttachmentJob?> {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.thoughtcrime.securesms.keyvalue
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.backup.RestoreState
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupFrequency
|
||||
@@ -65,7 +66,6 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) {
|
||||
var nextBackupTime: Long by longValue(KEY_NEXT_BACKUP_TIME, -1)
|
||||
var lastBackupTime: Long by longValue(KEY_LAST_BACKUP_TIME, -1)
|
||||
var lastMediaSyncTime: Long by longValue(KEY_LAST_BACKUP_MEDIA_SYNC_TIME, -1)
|
||||
var totalRestorableAttachmentSize: Long by longValue(KEY_TOTAL_RESTORABLE_ATTACHMENT_SIZE, 0)
|
||||
var backupFrequency: BackupFrequency by enumValue(KEY_BACKUP_FREQUENCY, BackupFrequency.MANUAL, BackupFrequency.Serializer)
|
||||
var backupTier: MessageBackupTier? by enumValue(KEY_BACKUP_TIER, null, MessageBackupTier.Serializer)
|
||||
|
||||
@@ -96,6 +96,11 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) {
|
||||
|
||||
var backupsInitialized: Boolean by booleanValue(KEY_BACKUPS_INITIALIZED, false)
|
||||
|
||||
private val totalRestorableAttachmentSizeValue = longValue(KEY_TOTAL_RESTORABLE_ATTACHMENT_SIZE, 0)
|
||||
var totalRestorableAttachmentSize: Long by totalRestorableAttachmentSizeValue
|
||||
val totalRestorableAttachmentSizeFlow: Flow<Long>
|
||||
get() = totalRestorableAttachmentSizeValue.toFlow()
|
||||
|
||||
val isRestoreInProgress: Boolean
|
||||
get() = totalRestorableAttachmentSize > 0
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package org.thoughtcrime.securesms.keyvalue
|
||||
|
||||
import com.squareup.wire.ProtoAdapter
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import org.signal.core.util.LongSerializer
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
@@ -45,12 +47,22 @@ internal fun <M> SignalStoreValues.protoValue(key: String, adapter: ProtoAdapter
|
||||
* class to callers and protect the individual implementations as private behind the various extension functions.
|
||||
*/
|
||||
sealed class SignalStoreValueDelegate<T>(private val store: KeyValueStore) {
|
||||
|
||||
private var flow: Lazy<MutableStateFlow<T>> = lazy { MutableStateFlow(getValue(store)) }
|
||||
|
||||
operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
|
||||
return getValue(store)
|
||||
}
|
||||
|
||||
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
|
||||
setValue(store, value)
|
||||
if (flow.isInitialized()) {
|
||||
flow.value.tryEmit(value)
|
||||
}
|
||||
}
|
||||
|
||||
fun toFlow(): Flow<T> {
|
||||
return flow.value
|
||||
}
|
||||
|
||||
internal abstract fun getValue(values: KeyValueStore): T
|
||||
|
||||
15
app/src/main/res/drawable/symbol_backup_error_24.xml
Normal file
15
app/src/main/res/drawable/symbol_backup_error_24.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<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="M12 2.88c-5.04 0-9.13 4.08-9.13 9.12 0 2.34 0.88 4.47 2.33 6.09l0.48-0.48c0.41-0.4 1.11-0.21 1.26 0.35l0.76 2.96c0.14 0.56-0.37 1.06-0.92 0.92l-2.96-0.76c-0.57-0.15-0.76-0.85-0.35-1.26l0.5-0.5C2.2 17.4 1.11 14.83 1.11 12 1.13 6 6 1.12 12 1.12 18 1.13 22.88 6 22.88 12c0 6-4.87 10.88-10.88 10.88-0.48 0-0.88-0.4-0.88-0.88s0.4-0.88 0.88-0.88c5.04 0 9.13-4.08 9.13-9.12 0-5.04-4.09-9.13-9.13-9.13Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M12 6.38c-0.7 0-1.24 0.59-1.2 1.28l0.43 5.5c0.03 0.4 0.37 0.71 0.77 0.71s0.74-0.3 0.77-0.7l0.42-5.5c0.05-0.7-0.5-1.3-1.19-1.3Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M10.75 16.38c0-0.7 0.56-1.25 1.25-1.25s1.25 0.55 1.25 1.24c0 0.7-0.56 1.25-1.25 1.25s-1.25-0.55-1.25-1.25Z"/>
|
||||
</vector>
|
||||
@@ -7288,7 +7288,23 @@
|
||||
|
||||
<!-- BackupStatus -->
|
||||
<!-- Status title when user does not have enough free space to download their media. Placeholder is required disk space. -->
|
||||
<string name="BackupStatus__free_up_s_of_space_to_download_your_media">Free up %1$s of space to download your media.</string>
|
||||
<string name="BackupStatus__free_up_s_of_space_to_download_your_media">Free up %1$s of space to restore your media.</string>
|
||||
<!-- Status title for banner when user is actively restoring media from a backup -->
|
||||
<string name="BackupStatus__restoring_media">Restoring media</string>
|
||||
<!-- Status title for banner when user has paused restore media from a backup -->
|
||||
<string name="BackupStatus__restore_paused">Restore paused</string>
|
||||
<!-- Status title for banner when user has completed restoring restore media from a backup -->
|
||||
<string name="BackupStatus__restore_complete">Restore 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 -->
|
||||
<string name="BackupStatus__status_no_internet">No internet…</string>
|
||||
<!-- Status subtitle for banner when restoring media pauses for low battery -->
|
||||
<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>
|
||||
|
||||
|
||||
<!-- BackupsTypeSettingsFragment -->
|
||||
<!-- Displayed as the user\'s payment method as a label in a preference row -->
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
|
||||
package org.signal.core.util
|
||||
|
||||
import java.text.NumberFormat
|
||||
import kotlin.math.min
|
||||
|
||||
inline val Long.bytes: ByteSize
|
||||
get() = ByteSize(this)
|
||||
|
||||
@@ -15,41 +18,41 @@ inline val Long.kibiBytes: ByteSize
|
||||
get() = (this * 1024).bytes
|
||||
|
||||
inline val Int.kibiBytes: ByteSize
|
||||
get() = (this * 1024).bytes
|
||||
get() = (this.toLong() * 1024L).bytes
|
||||
|
||||
inline val Long.mebiBytes: ByteSize
|
||||
get() = (this * 1024).kibiBytes
|
||||
get() = (this * 1024L).kibiBytes
|
||||
|
||||
inline val Int.mebiBytes: ByteSize
|
||||
get() = (this * 1024).kibiBytes
|
||||
get() = (this.toLong() * 1024L).kibiBytes
|
||||
|
||||
inline val Long.gibiBytes: ByteSize
|
||||
get() = (this * 1024).mebiBytes
|
||||
get() = (this * 1024L).mebiBytes
|
||||
|
||||
inline val Int.gibiBytes: ByteSize
|
||||
get() = (this * 1024).mebiBytes
|
||||
get() = (this.toLong() * 1024L).mebiBytes
|
||||
|
||||
inline val Long.tebiBytes: ByteSize
|
||||
get() = (this * 1024).gibiBytes
|
||||
get() = (this * 1024L).gibiBytes
|
||||
|
||||
inline val Int.tebiBytes: ByteSize
|
||||
get() = (this * 1024).gibiBytes
|
||||
get() = (this.toLong() * 1024L).gibiBytes
|
||||
|
||||
class ByteSize(val bytes: Long) {
|
||||
val inWholeBytes: Long
|
||||
get() = bytes
|
||||
|
||||
val inWholeKibiBytes: Long
|
||||
get() = bytes / 1024
|
||||
get() = bytes / 1024L
|
||||
|
||||
val inWholeMebiBytes: Long
|
||||
get() = inWholeKibiBytes / 1024
|
||||
get() = inWholeKibiBytes / 1024L
|
||||
|
||||
val inWholeGibiBytes: Long
|
||||
get() = inWholeMebiBytes / 1024
|
||||
get() = inWholeMebiBytes / 1024L
|
||||
|
||||
val inWholeTebiBytes: Long
|
||||
get() = inWholeGibiBytes / 1024
|
||||
get() = inWholeGibiBytes / 1024L
|
||||
|
||||
val inKibiBytes: Float
|
||||
get() = bytes / 1024f
|
||||
@@ -63,16 +66,35 @@ class ByteSize(val bytes: Long) {
|
||||
val inTebiBytes: Float
|
||||
get() = inGibiBytes / 1024f
|
||||
|
||||
fun getLargestNonZeroValue(): Pair<Long, Size> {
|
||||
fun getLargestNonZeroValue(): Pair<Float, Size> {
|
||||
return when {
|
||||
inWholeTebiBytes > 0L -> inWholeTebiBytes to Size.TEBIBYTE
|
||||
inWholeGibiBytes > 0L -> inWholeGibiBytes to Size.GIBIBYTE
|
||||
inWholeMebiBytes > 0L -> inWholeMebiBytes to Size.MEBIBYTE
|
||||
inWholeKibiBytes > 0L -> inWholeKibiBytes to Size.KIBIBYTE
|
||||
else -> inWholeBytes to Size.BYTE
|
||||
inWholeTebiBytes > 0L -> inTebiBytes to Size.TEBIBYTE
|
||||
inWholeGibiBytes > 0L -> inGibiBytes to Size.GIBIBYTE
|
||||
inWholeMebiBytes > 0L -> inMebiBytes to Size.MEBIBYTE
|
||||
inWholeKibiBytes > 0L -> inKibiBytes to Size.KIBIBYTE
|
||||
else -> inWholeBytes.toFloat() to Size.BYTE
|
||||
}
|
||||
}
|
||||
|
||||
fun toUnitString(maxPlaces: Int = 1, spaced: Boolean = true): String {
|
||||
val (size, unit) = getLargestNonZeroValue()
|
||||
|
||||
val formatter = NumberFormat.getInstance().apply {
|
||||
minimumFractionDigits = 0
|
||||
maximumFractionDigits = when (unit) {
|
||||
Size.BYTE,
|
||||
Size.KIBIBYTE -> 0
|
||||
|
||||
Size.MEBIBYTE -> min(1, maxPlaces)
|
||||
|
||||
Size.GIBIBYTE,
|
||||
Size.TEBIBYTE -> min(2, maxPlaces)
|
||||
}
|
||||
}
|
||||
|
||||
return "${formatter.format(size)}${if (spaced) " " else ""}${unit.label}"
|
||||
}
|
||||
|
||||
enum class Size(val label: String) {
|
||||
BYTE("B"),
|
||||
KIBIBYTE("KB"),
|
||||
|
||||
Reference in New Issue
Block a user