mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-20 19:18:37 +00:00
Improve display and management of backup progress.
This commit is contained in:
committed by
Cody Henthorne
parent
5b18f05aa8
commit
dd1697de41
@@ -5,21 +5,20 @@
|
|||||||
|
|
||||||
package org.thoughtcrime.securesms.backup
|
package org.thoughtcrime.securesms.backup
|
||||||
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
|
||||||
import kotlinx.coroutines.flow.flowOn
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.flow.shareIn
|
import org.signal.core.util.logging.Log
|
||||||
import org.signal.core.util.throttleLatest
|
import org.signal.core.util.throttleLatest
|
||||||
|
import org.thoughtcrime.securesms.attachments.AttachmentId
|
||||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
import org.thoughtcrime.securesms.keyvalue.protos.ArchiveUploadProgressState
|
import org.thoughtcrime.securesms.keyvalue.protos.ArchiveUploadProgressState
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
@@ -28,6 +27,8 @@ import kotlin.time.Duration.Companion.milliseconds
|
|||||||
*/
|
*/
|
||||||
object ArchiveUploadProgress {
|
object ArchiveUploadProgress {
|
||||||
|
|
||||||
|
private val TAG = Log.tag(ArchiveUploadProgress::class)
|
||||||
|
|
||||||
private val PROGRESS_NONE = ArchiveUploadProgressState(
|
private val PROGRESS_NONE = ArchiveUploadProgressState(
|
||||||
state = ArchiveUploadProgressState.State.None
|
state = ArchiveUploadProgressState.State.None
|
||||||
)
|
)
|
||||||
@@ -36,18 +37,30 @@ object ArchiveUploadProgress {
|
|||||||
|
|
||||||
private var uploadProgress: ArchiveUploadProgressState = SignalStore.backup.archiveUploadState ?: PROGRESS_NONE
|
private var uploadProgress: ArchiveUploadProgressState = SignalStore.backup.archiveUploadState ?: PROGRESS_NONE
|
||||||
|
|
||||||
|
private val partialMediaProgress: MutableMap<AttachmentId, Long> = ConcurrentHashMap()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Observe this to get updates on the current upload progress.
|
* Observe this to get updates on the current upload progress.
|
||||||
*/
|
*/
|
||||||
val progress: Flow<ArchiveUploadProgressState> = _progress
|
val progress: Flow<ArchiveUploadProgressState> = _progress
|
||||||
.throttleLatest(500.milliseconds)
|
.throttleLatest(500.milliseconds) {
|
||||||
|
uploadProgress.state == ArchiveUploadProgressState.State.None ||
|
||||||
|
(uploadProgress.state == ArchiveUploadProgressState.State.UploadBackupFile && uploadProgress.backupFileUploadedBytes == 0L) ||
|
||||||
|
(uploadProgress.state == ArchiveUploadProgressState.State.UploadMedia && uploadProgress.mediaUploadedBytes == 0L)
|
||||||
|
}
|
||||||
.map {
|
.map {
|
||||||
if (uploadProgress.state != ArchiveUploadProgressState.State.UploadingAttachments) {
|
if (uploadProgress.state != ArchiveUploadProgressState.State.UploadMedia) {
|
||||||
return@map uploadProgress
|
return@map uploadProgress
|
||||||
}
|
}
|
||||||
|
|
||||||
val pendingCount = SignalDatabase.attachments.getPendingArchiveUploadCount()
|
if (!SignalStore.backup.backsUpMedia) {
|
||||||
if (pendingCount == uploadProgress.totalAttachments) {
|
Log.i(TAG, "Doesn't upload media. Done!")
|
||||||
|
return@map PROGRESS_NONE
|
||||||
|
}
|
||||||
|
|
||||||
|
val pendingMediaUploadBytes = SignalDatabase.attachments.getPendingArchiveUploadBytes() - partialMediaProgress.values.sum()
|
||||||
|
if (pendingMediaUploadBytes <= 0) {
|
||||||
|
Log.i(TAG, "No more pending bytes. Done!")
|
||||||
return@map PROGRESS_NONE
|
return@map PROGRESS_NONE
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,90 +68,96 @@ object ArchiveUploadProgress {
|
|||||||
// If we wanted the most accurate progress possible, we could maintain a new database flag that indicates whether an attachment has been flagged as part
|
// If we wanted the most accurate progress possible, we could maintain a new database flag that indicates whether an attachment has been flagged as part
|
||||||
// of the current upload batch. However, this gets us pretty close while keeping things simple and not having to juggle extra flags, with the caveat that
|
// of the current upload batch. However, this gets us pretty close while keeping things simple and not having to juggle extra flags, with the caveat that
|
||||||
// the progress bar may occasionally be including media that is not actually referenced in the active backup file.
|
// the progress bar may occasionally be including media that is not actually referenced in the active backup file.
|
||||||
val totalCount = max(uploadProgress.totalAttachments, pendingCount)
|
val totalMediaUploadBytes = max(uploadProgress.mediaTotalBytes, pendingMediaUploadBytes)
|
||||||
|
|
||||||
ArchiveUploadProgressState(
|
ArchiveUploadProgressState(
|
||||||
state = ArchiveUploadProgressState.State.UploadingAttachments,
|
state = ArchiveUploadProgressState.State.UploadMedia,
|
||||||
completedAttachments = totalCount - pendingCount,
|
mediaUploadedBytes = totalMediaUploadBytes - pendingMediaUploadBytes,
|
||||||
totalAttachments = totalCount
|
mediaTotalBytes = totalMediaUploadBytes
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.onEach {
|
.onEach { updated ->
|
||||||
updateState(it, notify = false)
|
updateState(notify = false) { updated }
|
||||||
}
|
}
|
||||||
.flowOn(Dispatchers.IO)
|
.flowOn(Dispatchers.IO)
|
||||||
.shareIn(scope = CoroutineScope(Dispatchers.IO), started = SharingStarted.WhileSubscribed(), replay = 1)
|
|
||||||
|
|
||||||
val inProgress
|
val inProgress
|
||||||
get() = uploadProgress.state != ArchiveUploadProgressState.State.None
|
get() = uploadProgress.state != ArchiveUploadProgressState.State.None
|
||||||
|
|
||||||
fun begin() {
|
fun begin() {
|
||||||
updateState(
|
updateState {
|
||||||
ArchiveUploadProgressState(
|
ArchiveUploadProgressState(
|
||||||
state = ArchiveUploadProgressState.State.BackingUpMessages
|
state = ArchiveUploadProgressState.State.Export
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onMessageBackupCreated() {
|
fun onMessageBackupCreated(backupFileSize: Long) {
|
||||||
updateState(
|
updateState {
|
||||||
ArchiveUploadProgressState(
|
it.copy(
|
||||||
state = ArchiveUploadProgressState.State.UploadingMessages
|
state = ArchiveUploadProgressState.State.UploadBackupFile,
|
||||||
|
backupFileTotalBytes = backupFileSize,
|
||||||
|
backupFileUploadedBytes = 0
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onAttachmentsStarted(attachmentCount: Long) {
|
fun onMessageBackupUploadProgress(totalBytes: Long, bytesUploaded: Long) {
|
||||||
updateState(
|
updateState {
|
||||||
ArchiveUploadProgressState(
|
it.copy(
|
||||||
state = ArchiveUploadProgressState.State.UploadingAttachments,
|
state = ArchiveUploadProgressState.State.UploadBackupFile,
|
||||||
completedAttachments = 0,
|
backupFileUploadedBytes = bytesUploaded,
|
||||||
totalAttachments = attachmentCount
|
backupFileTotalBytes = totalBytes
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onAttachmentFinished() {
|
fun onAttachmentsStarted(totalAttachmentBytes: Long) {
|
||||||
|
updateState {
|
||||||
|
it.copy(
|
||||||
|
state = ArchiveUploadProgressState.State.UploadMedia,
|
||||||
|
mediaUploadedBytes = 0,
|
||||||
|
mediaTotalBytes = totalAttachmentBytes
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onAttachmentProgress(attachmentId: AttachmentId, bytesUploaded: Long) {
|
||||||
|
partialMediaProgress[attachmentId] = bytesUploaded
|
||||||
|
_progress.tryEmit(Unit)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onAttachmentFinished(attachmentId: AttachmentId) {
|
||||||
|
partialMediaProgress.remove(attachmentId)
|
||||||
_progress.tryEmit(Unit)
|
_progress.tryEmit(Unit)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onMessageBackupFinishedEarly() {
|
fun onMessageBackupFinishedEarly() {
|
||||||
updateState(PROGRESS_NONE)
|
updateState { PROGRESS_NONE }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onValidationFailure() {
|
fun onValidationFailure() {
|
||||||
updateState(PROGRESS_NONE)
|
updateState { PROGRESS_NONE }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onMainBackupFileUploadFailure() {
|
fun onMainBackupFileUploadFailure() {
|
||||||
updateState(PROGRESS_NONE)
|
updateState { PROGRESS_NONE }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateState(state: ArchiveUploadProgressState, notify: Boolean = true) {
|
private fun updateState(notify: Boolean = true, transform: (ArchiveUploadProgressState) -> ArchiveUploadProgressState) {
|
||||||
uploadProgress = state
|
val newState = transform(uploadProgress)
|
||||||
SignalStore.backup.archiveUploadState = state
|
if (uploadProgress == newState) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadProgress = newState
|
||||||
|
SignalStore.backup.archiveUploadState = newState
|
||||||
|
|
||||||
if (notify) {
|
if (notify) {
|
||||||
_progress.tryEmit(Unit)
|
_progress.tryEmit(Unit)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ArchiveUploadProgressListener(
|
|
||||||
private val shouldCancel: () -> Boolean = { false }
|
|
||||||
) : SignalServiceAttachment.ProgressListener {
|
|
||||||
override fun onAttachmentProgress(total: Long, progress: Long) {
|
|
||||||
updateState(
|
|
||||||
state = ArchiveUploadProgressState(
|
|
||||||
state = ArchiveUploadProgressState.State.UploadingMessages,
|
|
||||||
totalAttachments = total,
|
|
||||||
completedAttachments = progress
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun shouldCancel(): Boolean = shouldCancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
object ArchiveBackupProgressListener : BackupRepository.ExportProgressListener {
|
object ArchiveBackupProgressListener : BackupRepository.ExportProgressListener {
|
||||||
override fun onAccount() {
|
override fun onAccount() {
|
||||||
updatePhase(ArchiveUploadProgressState.BackupPhase.Account)
|
updatePhase(ArchiveUploadProgressState.BackupPhase.Account)
|
||||||
@@ -178,17 +197,17 @@ object ArchiveUploadProgress {
|
|||||||
|
|
||||||
private fun updatePhase(
|
private fun updatePhase(
|
||||||
phase: ArchiveUploadProgressState.BackupPhase,
|
phase: ArchiveUploadProgressState.BackupPhase,
|
||||||
completedObjects: Long = 0L,
|
exportedFrames: Long = 0L,
|
||||||
totalObjects: Long = 0L
|
totalFrames: Long = 0L
|
||||||
) {
|
) {
|
||||||
updateState(
|
updateState {
|
||||||
state = ArchiveUploadProgressState(
|
ArchiveUploadProgressState(
|
||||||
state = ArchiveUploadProgressState.State.BackingUpMessages,
|
state = ArchiveUploadProgressState.State.Export,
|
||||||
backupPhase = phase,
|
backupPhase = phase,
|
||||||
completedAttachments = completedObjects,
|
frameExportCount = exportedFrames,
|
||||||
totalAttachments = totalObjects
|
frameTotalCount = totalFrames
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ import android.widget.Toast
|
|||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.biometric.BiometricManager
|
import androidx.biometric.BiometricManager
|
||||||
import androidx.biometric.BiometricPrompt
|
import androidx.biometric.BiometricPrompt
|
||||||
import androidx.compose.animation.AnimatedContent
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
@@ -64,6 +65,7 @@ import androidx.compose.ui.text.buildAnnotatedString
|
|||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.DialogProperties
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.navigation.fragment.navArgs
|
import androidx.navigation.fragment.navArgs
|
||||||
import org.signal.core.ui.compose.Buttons
|
import org.signal.core.ui.compose.Buttons
|
||||||
@@ -108,7 +110,6 @@ import org.thoughtcrime.securesms.util.viewModel
|
|||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
import java.util.Currency
|
import java.util.Currency
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import kotlin.math.max
|
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
import kotlin.time.Duration.Companion.days
|
import kotlin.time.Duration.Companion.days
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
@@ -137,7 +138,7 @@ class RemoteBackupsSettingsFragment : ComposeFragment() {
|
|||||||
@Composable
|
@Composable
|
||||||
override fun FragmentContent() {
|
override fun FragmentContent() {
|
||||||
val state by viewModel.state.collectAsState()
|
val state by viewModel.state.collectAsState()
|
||||||
val backupProgress by ArchiveUploadProgress.progress.collectAsState(initial = null)
|
val backupProgress by ArchiveUploadProgress.progress.collectAsStateWithLifecycle(initialValue = null)
|
||||||
val restoreState by viewModel.restoreState.collectAsState()
|
val restoreState by viewModel.restoreState.collectAsState()
|
||||||
val callbacks = remember { Callbacks() }
|
val callbacks = remember { Callbacks() }
|
||||||
|
|
||||||
@@ -378,36 +379,34 @@ private fun RemoteBackupsSettingsContent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
item {
|
item {
|
||||||
AnimatedContent(backupState, label = "backup-state-block") { state ->
|
when (backupState) {
|
||||||
when (state) {
|
is RemoteBackupsSettingsState.BackupState.Loading -> {
|
||||||
is RemoteBackupsSettingsState.BackupState.Loading -> {
|
LoadingCard()
|
||||||
LoadingCard()
|
}
|
||||||
}
|
|
||||||
|
|
||||||
is RemoteBackupsSettingsState.BackupState.Error -> {
|
is RemoteBackupsSettingsState.BackupState.Error -> {
|
||||||
ErrorCard()
|
ErrorCard()
|
||||||
}
|
}
|
||||||
|
|
||||||
is RemoteBackupsSettingsState.BackupState.Pending -> {
|
is RemoteBackupsSettingsState.BackupState.Pending -> {
|
||||||
PendingCard(state.price)
|
PendingCard(backupState.price)
|
||||||
}
|
}
|
||||||
|
|
||||||
is RemoteBackupsSettingsState.BackupState.SubscriptionMismatchMissingGooglePlay -> {
|
is RemoteBackupsSettingsState.BackupState.SubscriptionMismatchMissingGooglePlay -> {
|
||||||
SubscriptionMismatchMissingGooglePlayCard(
|
SubscriptionMismatchMissingGooglePlayCard(
|
||||||
state = state,
|
state = backupState,
|
||||||
onLearnMoreClick = contentCallbacks::onLearnMoreAboutLostSubscription,
|
onLearnMoreClick = contentCallbacks::onLearnMoreAboutLostSubscription,
|
||||||
onRenewClick = contentCallbacks::onRenewLostSubscription
|
onRenewClick = contentCallbacks::onRenewLostSubscription
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
RemoteBackupsSettingsState.BackupState.None -> Unit
|
RemoteBackupsSettingsState.BackupState.None -> Unit
|
||||||
|
|
||||||
is RemoteBackupsSettingsState.BackupState.WithTypeAndRenewalTime -> {
|
is RemoteBackupsSettingsState.BackupState.WithTypeAndRenewalTime -> {
|
||||||
BackupCard(
|
BackupCard(
|
||||||
backupState = state,
|
backupState = backupState,
|
||||||
onBackupTypeActionButtonClicked = contentCallbacks::onBackupTypeActionClick
|
onBackupTypeActionButtonClicked = contentCallbacks::onBackupTypeActionClick
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -993,14 +992,26 @@ private fun InProgressBackupRow(
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
) {
|
) {
|
||||||
val backupProgress = getBackupProgress(archiveUploadProgressState)
|
when (archiveUploadProgressState.state) {
|
||||||
if (backupProgress.total == 0L) {
|
ArchiveUploadProgressState.State.None -> {
|
||||||
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
|
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
|
||||||
} else {
|
}
|
||||||
LinearProgressIndicator(
|
ArchiveUploadProgressState.State.Export -> {
|
||||||
modifier = Modifier.fillMaxWidth(),
|
val progressValue by animateFloatAsState(targetValue = archiveUploadProgressState.frameExportProgress(), animationSpec = tween(durationMillis = 250))
|
||||||
progress = { backupProgress.progress }
|
LinearProgressIndicator(
|
||||||
)
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
progress = { progressValue },
|
||||||
|
drawStopIndicator = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ArchiveUploadProgressState.State.UploadBackupFile, ArchiveUploadProgressState.State.UploadMedia -> {
|
||||||
|
val progressValue by animateFloatAsState(targetValue = archiveUploadProgressState.uploadProgress(), animationSpec = tween(durationMillis = 250))
|
||||||
|
LinearProgressIndicator(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
progress = { progressValue },
|
||||||
|
drawStopIndicator = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
@@ -1012,33 +1023,26 @@ private fun InProgressBackupRow(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getBackupProgress(state: ArchiveUploadProgressState): BackupProgress {
|
|
||||||
val approximateMessageCount = max(state.completedAttachments, state.totalAttachments)
|
|
||||||
return BackupProgress(state.completedAttachments, approximateMessageCount)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun getProgressStateMessage(archiveUploadProgressState: ArchiveUploadProgressState): String {
|
private fun getProgressStateMessage(archiveUploadProgressState: ArchiveUploadProgressState): String {
|
||||||
return when (archiveUploadProgressState.state) {
|
return when (archiveUploadProgressState.state) {
|
||||||
ArchiveUploadProgressState.State.None -> stringResource(R.string.RemoteBackupsSettingsFragment__processing_backup)
|
ArchiveUploadProgressState.State.None -> stringResource(R.string.RemoteBackupsSettingsFragment__processing_backup)
|
||||||
ArchiveUploadProgressState.State.BackingUpMessages -> getBackupPhaseMessage(archiveUploadProgressState)
|
ArchiveUploadProgressState.State.Export -> getBackupExportPhaseProgressString(archiveUploadProgressState)
|
||||||
ArchiveUploadProgressState.State.UploadingMessages -> getUploadingMessages(archiveUploadProgressState)
|
ArchiveUploadProgressState.State.UploadBackupFile, ArchiveUploadProgressState.State.UploadMedia -> getBackupUploadPhaseProgressString(archiveUploadProgressState)
|
||||||
ArchiveUploadProgressState.State.UploadingAttachments -> getUploadingAttachmentsMessage(archiveUploadProgressState)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun getBackupPhaseMessage(state: ArchiveUploadProgressState): String {
|
private fun getBackupExportPhaseProgressString(state: ArchiveUploadProgressState): String {
|
||||||
return when (state.backupPhase) {
|
return when (state.backupPhase) {
|
||||||
ArchiveUploadProgressState.BackupPhase.BackupPhaseNone -> stringResource(R.string.RemoteBackupsSettingsFragment__processing_backup)
|
ArchiveUploadProgressState.BackupPhase.BackupPhaseNone -> stringResource(R.string.RemoteBackupsSettingsFragment__processing_backup)
|
||||||
ArchiveUploadProgressState.BackupPhase.Message -> {
|
ArchiveUploadProgressState.BackupPhase.Message -> {
|
||||||
val progress = getBackupProgress(state)
|
|
||||||
pluralStringResource(
|
pluralStringResource(
|
||||||
R.plurals.RemoteBackupsSettingsFragment__processing_d_of_d_d_messages,
|
R.plurals.RemoteBackupsSettingsFragment__processing_messages_progress_text,
|
||||||
progress.total.toInt(),
|
state.frameTotalCount.toInt(),
|
||||||
"%,d".format(progress.completed.toInt()),
|
"%,d".format(state.frameExportCount),
|
||||||
"%,d".format(progress.total.toInt()),
|
"%,d".format(state.frameTotalCount),
|
||||||
(progress.progress * 100).toInt()
|
(state.frameExportProgress() * 100).toInt()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1047,25 +1051,12 @@ private fun getBackupPhaseMessage(state: ArchiveUploadProgressState): String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun getUploadingMessages(state: ArchiveUploadProgressState): String {
|
private fun getBackupUploadPhaseProgressString(state: ArchiveUploadProgressState): String {
|
||||||
val formattedCompleted = state.completedAttachments.bytes.toUnitString()
|
val formattedTotalBytes = state.uploadBytesTotal.bytes.toUnitString()
|
||||||
val formattedTotal = state.totalAttachments.bytes.toUnitString()
|
val formattedUploadedBytes = state.uploadBytesUploaded.bytes.toUnitString()
|
||||||
val percent = if (state.totalAttachments == 0L) {
|
val percent = (state.uploadProgress() * 100).toInt()
|
||||||
0
|
|
||||||
} else {
|
|
||||||
((state.completedAttachments / state.totalAttachments.toFloat()) * 100).toInt()
|
|
||||||
}
|
|
||||||
|
|
||||||
return stringResource(R.string.RemoteBackupsSettingsFragment__uploading_s_of_s_d, formattedCompleted, formattedTotal, percent)
|
return stringResource(R.string.RemoteBackupsSettingsFragment__uploading_s_of_s_d, formattedUploadedBytes, formattedTotalBytes, percent)
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun getUploadingAttachmentsMessage(state: ArchiveUploadProgressState): String {
|
|
||||||
return if (state.totalAttachments == 0L) {
|
|
||||||
stringResource(R.string.RemoteBackupsSettingsFragment__processing_backup)
|
|
||||||
} else {
|
|
||||||
stringResource(R.string.RemoteBackupsSettingsFragment__d_slash_d, state.completedAttachments, state.totalAttachments)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -1475,70 +1466,82 @@ private fun InProgressRowPreview() {
|
|||||||
InProgressBackupRow(archiveUploadProgressState = ArchiveUploadProgressState())
|
InProgressBackupRow(archiveUploadProgressState = ArchiveUploadProgressState())
|
||||||
InProgressBackupRow(
|
InProgressBackupRow(
|
||||||
archiveUploadProgressState = ArchiveUploadProgressState(
|
archiveUploadProgressState = ArchiveUploadProgressState(
|
||||||
state = ArchiveUploadProgressState.State.BackingUpMessages,
|
state = ArchiveUploadProgressState.State.Export,
|
||||||
backupPhase = ArchiveUploadProgressState.BackupPhase.BackupPhaseNone
|
backupPhase = ArchiveUploadProgressState.BackupPhase.BackupPhaseNone
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
InProgressBackupRow(
|
InProgressBackupRow(
|
||||||
archiveUploadProgressState = ArchiveUploadProgressState(
|
archiveUploadProgressState = ArchiveUploadProgressState(
|
||||||
state = ArchiveUploadProgressState.State.BackingUpMessages,
|
state = ArchiveUploadProgressState.State.Export,
|
||||||
backupPhase = ArchiveUploadProgressState.BackupPhase.Account
|
backupPhase = ArchiveUploadProgressState.BackupPhase.Account
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
InProgressBackupRow(
|
InProgressBackupRow(
|
||||||
archiveUploadProgressState = ArchiveUploadProgressState(
|
archiveUploadProgressState = ArchiveUploadProgressState(
|
||||||
state = ArchiveUploadProgressState.State.BackingUpMessages,
|
state = ArchiveUploadProgressState.State.Export,
|
||||||
backupPhase = ArchiveUploadProgressState.BackupPhase.Call
|
backupPhase = ArchiveUploadProgressState.BackupPhase.Call
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
InProgressBackupRow(
|
InProgressBackupRow(
|
||||||
archiveUploadProgressState = ArchiveUploadProgressState(
|
archiveUploadProgressState = ArchiveUploadProgressState(
|
||||||
state = ArchiveUploadProgressState.State.BackingUpMessages,
|
state = ArchiveUploadProgressState.State.Export,
|
||||||
backupPhase = ArchiveUploadProgressState.BackupPhase.Sticker
|
backupPhase = ArchiveUploadProgressState.BackupPhase.Sticker
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
InProgressBackupRow(
|
InProgressBackupRow(
|
||||||
archiveUploadProgressState = ArchiveUploadProgressState(
|
archiveUploadProgressState = ArchiveUploadProgressState(
|
||||||
state = ArchiveUploadProgressState.State.BackingUpMessages,
|
state = ArchiveUploadProgressState.State.Export,
|
||||||
backupPhase = ArchiveUploadProgressState.BackupPhase.Recipient
|
backupPhase = ArchiveUploadProgressState.BackupPhase.Recipient
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
InProgressBackupRow(
|
InProgressBackupRow(
|
||||||
archiveUploadProgressState = ArchiveUploadProgressState(
|
archiveUploadProgressState = ArchiveUploadProgressState(
|
||||||
state = ArchiveUploadProgressState.State.BackingUpMessages,
|
state = ArchiveUploadProgressState.State.Export,
|
||||||
backupPhase = ArchiveUploadProgressState.BackupPhase.Thread
|
backupPhase = ArchiveUploadProgressState.BackupPhase.Thread
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
InProgressBackupRow(
|
InProgressBackupRow(
|
||||||
archiveUploadProgressState = ArchiveUploadProgressState(
|
archiveUploadProgressState = ArchiveUploadProgressState(
|
||||||
state = ArchiveUploadProgressState.State.BackingUpMessages,
|
state = ArchiveUploadProgressState.State.Export,
|
||||||
backupPhase = ArchiveUploadProgressState.BackupPhase.Message,
|
backupPhase = ArchiveUploadProgressState.BackupPhase.Message,
|
||||||
completedAttachments = 1,
|
frameExportCount = 1,
|
||||||
totalAttachments = 1
|
frameTotalCount = 1
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
InProgressBackupRow(
|
InProgressBackupRow(
|
||||||
archiveUploadProgressState = ArchiveUploadProgressState(
|
archiveUploadProgressState = ArchiveUploadProgressState(
|
||||||
state = ArchiveUploadProgressState.State.BackingUpMessages,
|
state = ArchiveUploadProgressState.State.Export,
|
||||||
backupPhase = ArchiveUploadProgressState.BackupPhase.Message,
|
backupPhase = ArchiveUploadProgressState.BackupPhase.Message,
|
||||||
completedAttachments = 1000,
|
frameExportCount = 1000,
|
||||||
totalAttachments = 100_000
|
frameTotalCount = 100_000
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
InProgressBackupRow(
|
InProgressBackupRow(
|
||||||
archiveUploadProgressState = ArchiveUploadProgressState(
|
archiveUploadProgressState = ArchiveUploadProgressState(
|
||||||
state = ArchiveUploadProgressState.State.BackingUpMessages,
|
state = ArchiveUploadProgressState.State.Export,
|
||||||
backupPhase = ArchiveUploadProgressState.BackupPhase.Message,
|
backupPhase = ArchiveUploadProgressState.BackupPhase.Message,
|
||||||
completedAttachments = 1_000_000,
|
frameExportCount = 1_000_000,
|
||||||
totalAttachments = 100_000
|
frameTotalCount = 100_000
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
InProgressBackupRow(
|
InProgressBackupRow(
|
||||||
archiveUploadProgressState = ArchiveUploadProgressState(
|
archiveUploadProgressState = ArchiveUploadProgressState(
|
||||||
state = ArchiveUploadProgressState.State.UploadingMessages,
|
state = ArchiveUploadProgressState.State.UploadBackupFile,
|
||||||
backupPhase = ArchiveUploadProgressState.BackupPhase.BackupPhaseNone,
|
backupPhase = ArchiveUploadProgressState.BackupPhase.BackupPhaseNone,
|
||||||
completedAttachments = 1.gibiBytes.inWholeBytes + 100.mebiBytes.inWholeBytes,
|
backupFileUploadedBytes = 10.mebiBytes.inWholeBytes,
|
||||||
totalAttachments = 12.gibiBytes.inWholeBytes
|
backupFileTotalBytes = 50.mebiBytes.inWholeBytes,
|
||||||
|
mediaUploadedBytes = 0,
|
||||||
|
mediaTotalBytes = 0
|
||||||
|
)
|
||||||
|
)
|
||||||
|
InProgressBackupRow(
|
||||||
|
archiveUploadProgressState = ArchiveUploadProgressState(
|
||||||
|
state = ArchiveUploadProgressState.State.UploadMedia,
|
||||||
|
backupPhase = ArchiveUploadProgressState.BackupPhase.BackupPhaseNone,
|
||||||
|
backupFileUploadedBytes = 10.mebiBytes.inWholeBytes,
|
||||||
|
backupFileTotalBytes = 50.mebiBytes.inWholeBytes,
|
||||||
|
mediaUploadedBytes = 100.mebiBytes.inWholeBytes,
|
||||||
|
mediaTotalBytes = 1.gibiBytes.inWholeBytes
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1614,3 +1617,28 @@ private data class BackupProgress(
|
|||||||
) {
|
) {
|
||||||
val progress: Float = if (total > 0) completed / total.toFloat() else 0f
|
val progress: Float = if (total > 0) completed / total.toFloat() else 0f
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun ArchiveUploadProgressState.frameExportProgress(): Float {
|
||||||
|
return if (this.frameTotalCount == 0L) {
|
||||||
|
0f
|
||||||
|
} else {
|
||||||
|
this.frameExportCount / this.frameTotalCount.toFloat()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ArchiveUploadProgressState.uploadProgress(): Float {
|
||||||
|
val current = this.backupFileUploadedBytes + this.mediaUploadedBytes
|
||||||
|
val total = this.backupFileTotalBytes + this.mediaTotalBytes
|
||||||
|
|
||||||
|
return if (total == 0L) {
|
||||||
|
0f
|
||||||
|
} else {
|
||||||
|
current / total.toFloat()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val ArchiveUploadProgressState.uploadBytesTotal: Long
|
||||||
|
get() = this.backupFileTotalBytes + this.mediaTotalBytes
|
||||||
|
|
||||||
|
private val ArchiveUploadProgressState.uploadBytesUploaded: Long
|
||||||
|
get() = this.backupFileUploadedBytes + this.mediaUploadedBytes
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import org.signal.core.util.bytes
|
|||||||
import org.signal.core.util.logging.Log
|
import org.signal.core.util.logging.Log
|
||||||
import org.signal.core.util.money.FiatMoney
|
import org.signal.core.util.money.FiatMoney
|
||||||
import org.signal.donations.InAppPaymentType
|
import org.signal.donations.InAppPaymentType
|
||||||
|
import org.thoughtcrime.securesms.backup.ArchiveUploadProgress
|
||||||
import org.thoughtcrime.securesms.backup.v2.BackupFrequency
|
import org.thoughtcrime.securesms.backup.v2.BackupFrequency
|
||||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
||||||
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
||||||
@@ -41,6 +42,7 @@ import org.thoughtcrime.securesms.dependencies.AppDependencies
|
|||||||
import org.thoughtcrime.securesms.jobs.BackupMessagesJob
|
import org.thoughtcrime.securesms.jobs.BackupMessagesJob
|
||||||
import org.thoughtcrime.securesms.jobs.RestoreOptimizedMediaJob
|
import org.thoughtcrime.securesms.jobs.RestoreOptimizedMediaJob
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.protos.ArchiveUploadProgressState
|
||||||
import org.thoughtcrime.securesms.service.MessageBackupListener
|
import org.thoughtcrime.securesms.service.MessageBackupListener
|
||||||
import java.util.Currency
|
import java.util.Currency
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
@@ -103,6 +105,20 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
|
|||||||
delay(1.seconds)
|
delay(1.seconds)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
var previous: ArchiveUploadProgressState.State? = null
|
||||||
|
ArchiveUploadProgress.progress
|
||||||
|
.collect { current ->
|
||||||
|
if (previous != null && current.state == ArchiveUploadProgressState.State.None) {
|
||||||
|
_state.update {
|
||||||
|
it.copy(lastBackupTimestamp = SignalStore.backup.lastBackupTime)
|
||||||
|
}
|
||||||
|
refreshState(null)
|
||||||
|
}
|
||||||
|
previous = current.state
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setCanBackUpUsingCellular(canBackUpUsingCellular: Boolean) {
|
fun setCanBackUpUsingCellular(canBackUpUsingCellular: Boolean) {
|
||||||
@@ -154,6 +170,42 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun turnOffAndDeleteBackups() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
Log.d(TAG, "Beginning to turn off and delete backup.")
|
||||||
|
requestDialog(RemoteBackupsSettingsState.Dialog.PROGRESS_SPINNER)
|
||||||
|
|
||||||
|
val hasMediaBackupUploaded = SignalStore.backup.backsUpMedia && SignalStore.backup.hasBackupBeenUploaded
|
||||||
|
|
||||||
|
val succeeded = withContext(Dispatchers.IO) {
|
||||||
|
BackupRepository.turnOffAndDisableBackups()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isActive) {
|
||||||
|
if (succeeded) {
|
||||||
|
if (hasMediaBackupUploaded && SignalStore.backup.optimizeStorage) {
|
||||||
|
Log.d(TAG, "User has optimized storage, downloading.")
|
||||||
|
requestDialog(RemoteBackupsSettingsState.Dialog.DOWNLOADING_YOUR_BACKUP)
|
||||||
|
|
||||||
|
SignalStore.backup.optimizeStorage = false
|
||||||
|
RestoreOptimizedMediaJob.enqueue()
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "User does not have optimized storage, finished.")
|
||||||
|
requestDialog(RemoteBackupsSettingsState.Dialog.NONE)
|
||||||
|
}
|
||||||
|
refresh()
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "Failed to disable backups.")
|
||||||
|
requestDialog(RemoteBackupsSettingsState.Dialog.TURN_OFF_FAILED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onBackupNowClick() {
|
||||||
|
BackupMessagesJob.enqueue()
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun refreshState(lastPurchase: InAppPaymentTable.InAppPayment?) {
|
private suspend fun refreshState(lastPurchase: InAppPaymentTable.InAppPayment?) {
|
||||||
val tier = SignalStore.backup.latestBackupTier
|
val tier = SignalStore.backup.latestBackupTier
|
||||||
|
|
||||||
@@ -307,39 +359,6 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun turnOffAndDeleteBackups() {
|
private fun refreshLocalState() {
|
||||||
viewModelScope.launch {
|
|
||||||
Log.d(TAG, "Beginning to turn off and delete backup.")
|
|
||||||
requestDialog(RemoteBackupsSettingsState.Dialog.PROGRESS_SPINNER)
|
|
||||||
|
|
||||||
val hasMediaBackupUploaded = SignalStore.backup.backsUpMedia && SignalStore.backup.hasBackupBeenUploaded
|
|
||||||
|
|
||||||
val succeeded = withContext(Dispatchers.IO) {
|
|
||||||
BackupRepository.turnOffAndDisableBackups()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isActive) {
|
|
||||||
if (succeeded) {
|
|
||||||
if (hasMediaBackupUploaded && SignalStore.backup.optimizeStorage) {
|
|
||||||
Log.d(TAG, "User has optimized storage, downloading.")
|
|
||||||
requestDialog(RemoteBackupsSettingsState.Dialog.DOWNLOADING_YOUR_BACKUP)
|
|
||||||
|
|
||||||
SignalStore.backup.optimizeStorage = false
|
|
||||||
RestoreOptimizedMediaJob.enqueue()
|
|
||||||
} else {
|
|
||||||
Log.d(TAG, "User does not have optimized storage, finished.")
|
|
||||||
requestDialog(RemoteBackupsSettingsState.Dialog.NONE)
|
|
||||||
}
|
|
||||||
refresh()
|
|
||||||
} else {
|
|
||||||
Log.d(TAG, "Failed to disable backups.")
|
|
||||||
requestDialog(RemoteBackupsSettingsState.Dialog.TURN_OFF_FAILED)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onBackupNowClick() {
|
|
||||||
BackupMessagesJob.enqueue()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package org.thoughtcrime.securesms.components.settings.conversation
|
package org.thoughtcrime.securesms.components.settings.conversation
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
@@ -16,11 +17,14 @@ import org.signal.core.util.withinTransaction
|
|||||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
||||||
import org.thoughtcrime.securesms.MainActivity
|
import org.thoughtcrime.securesms.MainActivity
|
||||||
import org.thoughtcrime.securesms.R
|
import org.thoughtcrime.securesms.R
|
||||||
|
import org.thoughtcrime.securesms.attachments.Attachment
|
||||||
|
import org.thoughtcrime.securesms.attachments.UriAttachment
|
||||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
|
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
|
||||||
import org.thoughtcrime.securesms.components.settings.configure
|
import org.thoughtcrime.securesms.components.settings.configure
|
||||||
|
import org.thoughtcrime.securesms.database.AttachmentTable
|
||||||
import org.thoughtcrime.securesms.database.MessageType
|
import org.thoughtcrime.securesms.database.MessageType
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||||
@@ -31,14 +35,18 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore
|
|||||||
import org.thoughtcrime.securesms.mms.IncomingMessage
|
import org.thoughtcrime.securesms.mms.IncomingMessage
|
||||||
import org.thoughtcrime.securesms.mms.OutgoingMessage
|
import org.thoughtcrime.securesms.mms.OutgoingMessage
|
||||||
import org.thoughtcrime.securesms.profiles.AvatarHelper
|
import org.thoughtcrime.securesms.profiles.AvatarHelper
|
||||||
|
import org.thoughtcrime.securesms.providers.BlobProvider
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientForeverObserver
|
import org.thoughtcrime.securesms.recipients.RecipientForeverObserver
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
|
import org.thoughtcrime.securesms.util.BitmapUtil
|
||||||
|
import org.thoughtcrime.securesms.util.MediaUtil
|
||||||
import org.thoughtcrime.securesms.util.SpanUtil
|
import org.thoughtcrime.securesms.util.SpanUtil
|
||||||
import org.thoughtcrime.securesms.util.Util
|
import org.thoughtcrime.securesms.util.Util
|
||||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||||
import org.thoughtcrime.securesms.util.livedata.Store
|
import org.thoughtcrime.securesms.util.livedata.Store
|
||||||
import java.util.Objects
|
import java.util.Objects
|
||||||
|
import kotlin.random.Random
|
||||||
import kotlin.time.Duration.Companion.nanoseconds
|
import kotlin.time.Duration.Companion.nanoseconds
|
||||||
import kotlin.time.DurationUnit
|
import kotlin.time.DurationUnit
|
||||||
|
|
||||||
@@ -267,40 +275,71 @@ class InternalConversationSettingsFragment : DSLSettingsFragment(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
clickPref(
|
if (!recipient.isGroup) {
|
||||||
title = DSLSettingsText.from("Add 1,000 dummy messages"),
|
clickPref(
|
||||||
summary = DSLSettingsText.from("Just adds 1,000 random messages to the chat. Text-only, nothing complicated."),
|
title = DSLSettingsText.from("Add 1,000 dummy messages"),
|
||||||
onClick = {
|
summary = DSLSettingsText.from("Just adds 1,000 random messages to the chat. Text-only, nothing complicated."),
|
||||||
MaterialAlertDialogBuilder(requireContext())
|
onClick = {
|
||||||
.setTitle("Are you sure?")
|
MaterialAlertDialogBuilder(requireContext())
|
||||||
.setNegativeButton(android.R.string.cancel) { d, _ -> d.dismiss() }
|
.setTitle("Are you sure?")
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
.setNegativeButton(android.R.string.cancel) { d, _ -> d.dismiss() }
|
||||||
val messageCount = 1000
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
val startTime = System.currentTimeMillis() - messageCount
|
val messageCount = 1000
|
||||||
SignalDatabase.rawDatabase.withinTransaction {
|
val startTime = System.currentTimeMillis() - messageCount
|
||||||
val targetThread = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
|
SignalDatabase.rawDatabase.withinTransaction {
|
||||||
for (i in 1..messageCount) {
|
val targetThread = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
|
||||||
val time = startTime + i
|
for (i in 1..messageCount) {
|
||||||
if (Math.random() > 0.5) {
|
val time = startTime + i
|
||||||
|
if (Math.random() > 0.5) {
|
||||||
|
val id = SignalDatabase.messages.insertMessageOutbox(
|
||||||
|
message = OutgoingMessage(threadRecipient = recipient, sentTimeMillis = time, body = "Outgoing: $i"),
|
||||||
|
threadId = targetThread
|
||||||
|
)
|
||||||
|
SignalDatabase.messages.markAsSent(id, true)
|
||||||
|
} else {
|
||||||
|
SignalDatabase.messages.insertMessageInbox(
|
||||||
|
retrieved = IncomingMessage(type = MessageType.NORMAL, from = recipient.id, sentTimeMillis = time, serverTimeMillis = time, receivedTimeMillis = System.currentTimeMillis(), body = "Incoming: $i"),
|
||||||
|
candidateThreadId = targetThread
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Toast.makeText(context, "Done!", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
clickPref(
|
||||||
|
title = DSLSettingsText.from("Add 10 dummy messages with attachments"),
|
||||||
|
summary = DSLSettingsText.from("Adds 10 random messages to the chat with attachments of a random image. Attachments are not uploaded."),
|
||||||
|
onClick = {
|
||||||
|
MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setTitle("Are you sure?")
|
||||||
|
.setNegativeButton(android.R.string.cancel) { d, _ -> d.dismiss() }
|
||||||
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
val messageCount = 10
|
||||||
|
val startTime = System.currentTimeMillis() - messageCount
|
||||||
|
SignalDatabase.rawDatabase.withinTransaction {
|
||||||
|
val targetThread = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
|
||||||
|
for (i in 1..messageCount) {
|
||||||
|
val time = startTime + i
|
||||||
|
val attachment = makeDummyAttachment()
|
||||||
val id = SignalDatabase.messages.insertMessageOutbox(
|
val id = SignalDatabase.messages.insertMessageOutbox(
|
||||||
message = OutgoingMessage(threadRecipient = recipient, sentTimeMillis = time, body = "Outgoing: $i"),
|
message = OutgoingMessage(threadRecipient = recipient, sentTimeMillis = time, body = "Outgoing: $i", attachments = listOf(attachment)),
|
||||||
threadId = targetThread
|
threadId = targetThread
|
||||||
)
|
)
|
||||||
SignalDatabase.messages.markAsSent(id, true)
|
SignalDatabase.messages.markAsSent(id, true)
|
||||||
} else {
|
|
||||||
SignalDatabase.messages.insertMessageInbox(
|
|
||||||
retrieved = IncomingMessage(type = MessageType.NORMAL, from = recipient.id, sentTimeMillis = time, serverTimeMillis = time, receivedTimeMillis = System.currentTimeMillis(), body = "Incoming: $i"),
|
|
||||||
candidateThreadId = targetThread
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Toast.makeText(context, "Done!", Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, "Done!", Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (recipient.isSelf) {
|
if (recipient.isSelf) {
|
||||||
sectionHeaderPref(DSLSettingsText.from("Donations"))
|
sectionHeaderPref(DSLSettingsText.from("Donations"))
|
||||||
@@ -399,6 +438,37 @@ class InternalConversationSettingsFragment : DSLSettingsFragment(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun makeDummyAttachment(): Attachment {
|
||||||
|
val bitmapDimens = 1024
|
||||||
|
val bitmap = Bitmap.createBitmap(
|
||||||
|
IntArray(bitmapDimens * bitmapDimens) { Random.nextInt(0xFFFFFF) },
|
||||||
|
0,
|
||||||
|
bitmapDimens,
|
||||||
|
bitmapDimens,
|
||||||
|
bitmapDimens,
|
||||||
|
Bitmap.Config.RGB_565
|
||||||
|
)
|
||||||
|
val stream = BitmapUtil.toCompressedJpeg(bitmap)
|
||||||
|
val bytes = stream.readBytes()
|
||||||
|
val uri = BlobProvider.getInstance().forData(bytes).createForSingleSessionOnDisk(requireContext())
|
||||||
|
return UriAttachment(
|
||||||
|
uri = uri,
|
||||||
|
contentType = MediaUtil.IMAGE_JPEG,
|
||||||
|
transferState = AttachmentTable.TRANSFER_PROGRESS_DONE,
|
||||||
|
size = bytes.size.toLong(),
|
||||||
|
fileName = null,
|
||||||
|
voiceNote = false,
|
||||||
|
borderless = false,
|
||||||
|
videoGif = false,
|
||||||
|
quote = false,
|
||||||
|
caption = null,
|
||||||
|
stickerLocator = null,
|
||||||
|
blurHash = null,
|
||||||
|
audioHash = null,
|
||||||
|
transformProperties = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun copyToClipboard(text: String) {
|
private fun copyToClipboard(text: String) {
|
||||||
Util.copyToClipboard(requireContext(), text)
|
Util.copyToClipboard(requireContext(), text)
|
||||||
Toast.makeText(requireContext(), "Copied to clipboard", Toast.LENGTH_SHORT).show()
|
Toast.makeText(requireContext(), "Copied to clipboard", Toast.LENGTH_SHORT).show()
|
||||||
|
|||||||
@@ -718,14 +718,20 @@ class AttachmentTable(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the number of attachments that are in pending upload states to the archive cdn.
|
* Returns sum of the file sizes of attachments that are not fully uploaded to the archive CDN.
|
||||||
*/
|
*/
|
||||||
fun getPendingArchiveUploadCount(): Long {
|
fun getPendingArchiveUploadBytes(): Long {
|
||||||
return readableDatabase
|
return readableDatabase
|
||||||
.count()
|
.rawQuery(
|
||||||
.from(TABLE_NAME)
|
"""
|
||||||
.where("$ARCHIVE_TRANSFER_STATE IN (${ArchiveTransferState.UPLOAD_IN_PROGRESS.value}, ${ArchiveTransferState.COPY_PENDING.value})")
|
SELECT SUM($DATA_SIZE)
|
||||||
.run()
|
FROM (
|
||||||
|
SELECT DISTINCT $ARCHIVE_MEDIA_ID, $DATA_SIZE
|
||||||
|
FROM $TABLE_NAME
|
||||||
|
WHERE $ARCHIVE_TRANSFER_STATE NOT IN (${ArchiveTransferState.FINISHED.value}, ${ArchiveTransferState.PERMANENT_FAILURE.value})
|
||||||
|
)
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
.readToSingleLong()
|
.readToSingleLong()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ class ArchiveAttachmentBackfillJob private constructor(parameters: Parameters) :
|
|||||||
|
|
||||||
SignalDatabase.attachments.createKeyIvDigestForAttachmentsThatNeedArchiveUpload()
|
SignalDatabase.attachments.createKeyIvDigestForAttachmentsThatNeedArchiveUpload()
|
||||||
|
|
||||||
ArchiveUploadProgress.onAttachmentsStarted(jobs.size.toLong())
|
ArchiveUploadProgress.onAttachmentsStarted(SignalDatabase.attachments.getPendingArchiveUploadBytes())
|
||||||
|
|
||||||
Log.i(TAG, "Adding ${jobs.size} jobs to backfill attachments.")
|
Log.i(TAG, "Adding ${jobs.size} jobs to backfill attachments.")
|
||||||
AppDependencies.jobManager.addAll(jobs)
|
AppDependencies.jobManager.addAll(jobs)
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import org.thoughtcrime.securesms.jobs.protos.BackupMessagesJobData
|
|||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
import org.thoughtcrime.securesms.providers.BlobProvider
|
import org.thoughtcrime.securesms.providers.BlobProvider
|
||||||
import org.whispersystems.signalservice.api.NetworkResult
|
import org.whispersystems.signalservice.api.NetworkResult
|
||||||
|
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment
|
||||||
import org.whispersystems.signalservice.internal.push.AttachmentUploadForm
|
import org.whispersystems.signalservice.internal.push.AttachmentUploadForm
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
@@ -89,6 +90,10 @@ class BackupMessagesJob private constructor(
|
|||||||
|
|
||||||
override fun getFactoryKey(): String = KEY
|
override fun getFactoryKey(): String = KEY
|
||||||
|
|
||||||
|
override fun onAdded() {
|
||||||
|
ArchiveUploadProgress.begin()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onFailure() {
|
override fun onFailure() {
|
||||||
if (!isCanceled) {
|
if (!isCanceled) {
|
||||||
Log.w(TAG, "Failed to backup user messages. Marking failure state.")
|
Log.w(TAG, "Failed to backup user messages. Marking failure state.")
|
||||||
@@ -109,6 +114,8 @@ class BackupMessagesJob private constructor(
|
|||||||
BackupFileResult.Retry -> return Result.retry(defaultBackoff())
|
BackupFileResult.Retry -> return Result.retry(defaultBackoff())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ArchiveUploadProgress.onMessageBackupCreated(tempBackupFile.length())
|
||||||
|
|
||||||
this.syncTime = currentTime
|
this.syncTime = currentTime
|
||||||
this.dataFile = tempBackupFile.path
|
this.dataFile = tempBackupFile.path
|
||||||
|
|
||||||
@@ -134,8 +141,16 @@ class BackupMessagesJob private constructor(
|
|||||||
is NetworkResult.ApplicationError -> throw result.throwable
|
is NetworkResult.ApplicationError -> throw result.throwable
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val progressListener = object : SignalServiceAttachment.ProgressListener {
|
||||||
|
override fun onAttachmentProgress(total: Long, progress: Long) {
|
||||||
|
ArchiveUploadProgress.onMessageBackupUploadProgress(total, progress)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun shouldCancel(): Boolean = isCanceled
|
||||||
|
}
|
||||||
|
|
||||||
FileInputStream(tempBackupFile).use {
|
FileInputStream(tempBackupFile).use {
|
||||||
when (val result = BackupRepository.uploadBackupFile(backupSpec, it, tempBackupFile.length(), ArchiveUploadProgress.ArchiveUploadProgressListener { isCanceled })) {
|
when (val result = BackupRepository.uploadBackupFile(backupSpec, it, tempBackupFile.length(), progressListener)) {
|
||||||
is NetworkResult.Success -> {
|
is NetworkResult.Success -> {
|
||||||
Log.i(TAG, "Successfully uploaded backup file.")
|
Log.i(TAG, "Successfully uploaded backup file.")
|
||||||
SignalStore.backup.hasBackupBeenUploaded = true
|
SignalStore.backup.hasBackupBeenUploaded = true
|
||||||
@@ -204,7 +219,6 @@ class BackupMessagesJob private constructor(
|
|||||||
|
|
||||||
BlobProvider.getInstance().clearTemporaryBackupsDirectory(AppDependencies.application)
|
BlobProvider.getInstance().clearTemporaryBackupsDirectory(AppDependencies.application)
|
||||||
|
|
||||||
ArchiveUploadProgress.begin()
|
|
||||||
val tempBackupFile = BlobProvider.getInstance().forTemporaryBackup(AppDependencies.application)
|
val tempBackupFile = BlobProvider.getInstance().forTemporaryBackup(AppDependencies.application)
|
||||||
|
|
||||||
val outputStream = FileOutputStream(tempBackupFile)
|
val outputStream = FileOutputStream(tempBackupFile)
|
||||||
@@ -244,8 +258,6 @@ class BackupMessagesJob private constructor(
|
|||||||
return BackupFileResult.Failure
|
return BackupFileResult.Failure
|
||||||
}
|
}
|
||||||
|
|
||||||
ArchiveUploadProgress.onMessageBackupCreated()
|
|
||||||
|
|
||||||
return BackupFileResult.Success(tempBackupFile, currentTime)
|
return BackupFileResult.Success(tempBackupFile, currentTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ class CopyAttachmentToArchiveJob private constructor(private val attachmentId: A
|
|||||||
ArchiveThumbnailUploadJob.enqueueIfNecessary(attachmentId)
|
ArchiveThumbnailUploadJob.enqueueIfNecessary(attachmentId)
|
||||||
SignalStore.backup.usedBackupMediaSpace += AttachmentCipherStreamUtil.getCiphertextLength(PaddingInputStream.getPaddedSize(attachment.size))
|
SignalStore.backup.usedBackupMediaSpace += AttachmentCipherStreamUtil.getCiphertextLength(PaddingInputStream.getPaddedSize(attachment.size))
|
||||||
|
|
||||||
ArchiveUploadProgress.onAttachmentFinished()
|
ArchiveUploadProgress.onAttachmentFinished(attachmentId)
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
@@ -156,7 +156,7 @@ class CopyAttachmentToArchiveJob private constructor(private val attachmentId: A
|
|||||||
SignalDatabase.attachments.setArchiveTransferState(attachmentId, AttachmentTable.ArchiveTransferState.TEMPORARY_FAILURE)
|
SignalDatabase.attachments.setArchiveTransferState(attachmentId, AttachmentTable.ArchiveTransferState.TEMPORARY_FAILURE)
|
||||||
}
|
}
|
||||||
|
|
||||||
ArchiveUploadProgress.onAttachmentFinished()
|
ArchiveUploadProgress.onAttachmentFinished(attachmentId)
|
||||||
}
|
}
|
||||||
|
|
||||||
class Factory : Job.Factory<CopyAttachmentToArchiveJob> {
|
class Factory : Job.Factory<CopyAttachmentToArchiveJob> {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import org.signal.protos.resumableuploads.ResumableUpload
|
|||||||
import org.thoughtcrime.securesms.attachments.AttachmentId
|
import org.thoughtcrime.securesms.attachments.AttachmentId
|
||||||
import org.thoughtcrime.securesms.attachments.AttachmentUploadUtil
|
import org.thoughtcrime.securesms.attachments.AttachmentUploadUtil
|
||||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
|
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
|
||||||
|
import org.thoughtcrime.securesms.backup.ArchiveUploadProgress
|
||||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
||||||
import org.thoughtcrime.securesms.database.AttachmentTable
|
import org.thoughtcrime.securesms.database.AttachmentTable
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
@@ -24,6 +25,7 @@ import org.thoughtcrime.securesms.net.SignalNetwork
|
|||||||
import org.whispersystems.signalservice.api.NetworkResult
|
import org.whispersystems.signalservice.api.NetworkResult
|
||||||
import org.whispersystems.signalservice.api.archive.ArchiveMediaUploadFormStatusCodes
|
import org.whispersystems.signalservice.api.archive.ArchiveMediaUploadFormStatusCodes
|
||||||
import org.whispersystems.signalservice.api.attachment.AttachmentUploadResult
|
import org.whispersystems.signalservice.api.attachment.AttachmentUploadResult
|
||||||
|
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.net.ProtocolException
|
import java.net.ProtocolException
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
@@ -135,7 +137,11 @@ class UploadAttachmentToArchiveJob private constructor(
|
|||||||
context = context,
|
context = context,
|
||||||
attachment = attachment,
|
attachment = attachment,
|
||||||
uploadSpec = uploadSpec!!,
|
uploadSpec = uploadSpec!!,
|
||||||
cancellationSignal = { this.isCanceled }
|
cancellationSignal = { this.isCanceled },
|
||||||
|
progressListener = object : SignalServiceAttachment.ProgressListener {
|
||||||
|
override fun onAttachmentProgress(total: Long, progress: Long) = ArchiveUploadProgress.onAttachmentProgress(attachmentId, progress)
|
||||||
|
override fun shouldCancel() = this@UploadAttachmentToArchiveJob.isCanceled
|
||||||
|
}
|
||||||
)
|
)
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Log.e(TAG, "[$attachmentId] Failed to get attachment stream.", e)
|
Log.e(TAG, "[$attachmentId] Failed to get attachment stream.", e)
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ message LeastActiveLinkedDevice {
|
|||||||
message ArchiveUploadProgressState {
|
message ArchiveUploadProgressState {
|
||||||
enum State {
|
enum State {
|
||||||
None = 0;
|
None = 0;
|
||||||
BackingUpMessages = 1;
|
Export = 1;
|
||||||
UploadingMessages = 2;
|
UploadBackupFile = 2;
|
||||||
UploadingAttachments = 3;
|
UploadMedia = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -41,7 +41,11 @@ message ArchiveUploadProgressState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
State state = 1;
|
State state = 1;
|
||||||
uint64 completedAttachments = 2;
|
BackupPhase backupPhase = 2;
|
||||||
uint64 totalAttachments = 3;
|
uint64 frameExportCount = 3;
|
||||||
BackupPhase backupPhase = 4;
|
uint64 frameTotalCount = 4;
|
||||||
|
uint64 backupFileUploadedBytes = 5;
|
||||||
|
uint64 backupFileTotalBytes = 6;
|
||||||
|
uint64 mediaUploadedBytes = 7;
|
||||||
|
uint64 mediaTotalBytes = 8;
|
||||||
}
|
}
|
||||||
@@ -8043,9 +8043,9 @@
|
|||||||
<!-- Linear progress dialog text shown when preparing a backup -->
|
<!-- Linear progress dialog text shown when preparing a backup -->
|
||||||
<string name="RemoteBackupsSettingsFragment__preparing_backup">Preparing backup…</string>
|
<string name="RemoteBackupsSettingsFragment__preparing_backup">Preparing backup…</string>
|
||||||
<!-- Linear progress dialog text shown when processing messages for backup. First placeholder is completed count, second is approximate total count, third is percent completed. -->
|
<!-- Linear progress dialog text shown when processing messages for backup. First placeholder is completed count, second is approximate total count, third is percent completed. -->
|
||||||
<plurals name="RemoteBackupsSettingsFragment__processing_d_of_d_d_messages">
|
<plurals name="RemoteBackupsSettingsFragment__processing_messages_progress_text">
|
||||||
<item quantity="one">Processing %1$s of %2$s (%3$d%%) message</item>
|
<item quantity="one">Processing %1$s of %2$s message (%3$d%%)</item>
|
||||||
<item quantity="other">Processing %1$s of %2$s (%3$d%%) messages</item>
|
<item quantity="other">Processing %1$s of ~%2$s messages (%3$d%%)</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<!-- Displayed in row when backup is available for download and users subscription has expired. First placeholder is data size e.g. 12MB, second is days before expiration -->
|
<!-- Displayed in row when backup is available for download and users subscription has expired. First placeholder is data size e.g. 12MB, second is days before expiration -->
|
||||||
<plurals name="RemoteBackupsSettingsFragment__you_have_s_of_backup_data">
|
<plurals name="RemoteBackupsSettingsFragment__you_have_s_of_backup_data">
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ tasks.register("checkStopship") {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.awaitAll()
|
.awaitAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stopshipFiles.isNotEmpty()) {
|
if (stopshipFiles.isNotEmpty()) {
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ package org.signal.core.util
|
|||||||
|
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.channelFlow
|
||||||
import kotlinx.coroutines.flow.conflate
|
import kotlinx.coroutines.flow.conflate
|
||||||
|
import kotlinx.coroutines.flow.filterNot
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
|
|
||||||
@@ -16,9 +18,20 @@ import kotlin.time.Duration
|
|||||||
*
|
*
|
||||||
* You can think of this like debouncing, but with "checkpoints" so that even if you have a constant stream of values,
|
* You can think of this like debouncing, but with "checkpoints" so that even if you have a constant stream of values,
|
||||||
* you'll still get an emission every [timeout] (unlike debouncing, which will only emit once the stream settles down).
|
* you'll still get an emission every [timeout] (unlike debouncing, which will only emit once the stream settles down).
|
||||||
|
*
|
||||||
|
* You can specify an optional [emitImmediately] function that will indicate whether an emission should skip throttling and
|
||||||
|
* be emitted immediately. This lambda should be stateless, as it may be called multiple times for each item.
|
||||||
*/
|
*/
|
||||||
fun <T> Flow<T>.throttleLatest(timeout: Duration): Flow<T> {
|
fun <T> Flow<T>.throttleLatest(timeout: Duration, emitImmediately: (T) -> Boolean = { false }): Flow<T> {
|
||||||
return this
|
val rootFlow = this
|
||||||
.conflate()
|
return channelFlow {
|
||||||
.onEach { delay(timeout) }
|
rootFlow
|
||||||
|
.onEach { if (emitImmediately(it)) send(it) }
|
||||||
|
.filterNot { emitImmediately(it) }
|
||||||
|
.conflate()
|
||||||
|
.collect {
|
||||||
|
send(it)
|
||||||
|
delay(timeout)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,4 +60,20 @@ class FlowExtensionsTests {
|
|||||||
|
|
||||||
assertEquals(listOf(1, 5, 10, 15, 20, 25, 30), output)
|
assertEquals(listOf(1, 5, 10, 15, 20, 25, 30), output)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `throttleLatest - respects skipThrottle`() = runTest {
|
||||||
|
val testFlow = flow {
|
||||||
|
for (i in 1..30) {
|
||||||
|
emit(i)
|
||||||
|
delay(10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val output = testFlow
|
||||||
|
.throttleLatest(50.milliseconds) { it in setOf(2, 3, 4, 26, 27, 28) }
|
||||||
|
.toList()
|
||||||
|
|
||||||
|
assertEquals(listOf(1, 2, 3, 4, 5, 10, 15, 20, 25, 26, 27, 28, 30), output)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1080,9 +1080,9 @@ public class PushServiceSocket {
|
|||||||
|
|
||||||
public void uploadBackupFile(AttachmentUploadForm uploadForm, String resumableUploadUrl, InputStream data, long dataLength, ProgressListener progressListener) throws IOException {
|
public void uploadBackupFile(AttachmentUploadForm uploadForm, String resumableUploadUrl, InputStream data, long dataLength, ProgressListener progressListener) throws IOException {
|
||||||
if (uploadForm.cdn == 2) {
|
if (uploadForm.cdn == 2) {
|
||||||
uploadToCdn2(resumableUploadUrl, data, "application/octet-stream", dataLength, false, new NoCipherOutputStreamFactory(), null, null);
|
uploadToCdn2(resumableUploadUrl, data, "application/octet-stream", dataLength, false, new NoCipherOutputStreamFactory(), progressListener, null);
|
||||||
} else {
|
} else {
|
||||||
uploadToCdn3(resumableUploadUrl, data, "application/octet-stream", dataLength, false, new NoCipherOutputStreamFactory(), null, null, uploadForm.headers);
|
uploadToCdn3(resumableUploadUrl, data, "application/octet-stream", dataLength, false, new NoCipherOutputStreamFactory(), progressListener, null, uploadForm.headers);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user