Improve display and management of backup progress.

This commit is contained in:
Greyson Parrelli
2025-03-21 14:33:29 -04:00
committed by Cody Henthorne
parent 5b18f05aa8
commit dd1697de41
15 changed files with 433 additions and 240 deletions

View File

@@ -11,7 +11,8 @@ import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.biometric.BiometricManager
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.background
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.unit.dp
import androidx.compose.ui.window.DialogProperties
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import org.signal.core.ui.compose.Buttons
@@ -108,7 +110,6 @@ import org.thoughtcrime.securesms.util.viewModel
import java.math.BigDecimal
import java.util.Currency
import java.util.Locale
import kotlin.math.max
import kotlin.time.Duration
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.milliseconds
@@ -137,7 +138,7 @@ class RemoteBackupsSettingsFragment : ComposeFragment() {
@Composable
override fun FragmentContent() {
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 callbacks = remember { Callbacks() }
@@ -378,36 +379,34 @@ private fun RemoteBackupsSettingsContent(
}
item {
AnimatedContent(backupState, label = "backup-state-block") { state ->
when (state) {
is RemoteBackupsSettingsState.BackupState.Loading -> {
LoadingCard()
}
when (backupState) {
is RemoteBackupsSettingsState.BackupState.Loading -> {
LoadingCard()
}
is RemoteBackupsSettingsState.BackupState.Error -> {
ErrorCard()
}
is RemoteBackupsSettingsState.BackupState.Error -> {
ErrorCard()
}
is RemoteBackupsSettingsState.BackupState.Pending -> {
PendingCard(state.price)
}
is RemoteBackupsSettingsState.BackupState.Pending -> {
PendingCard(backupState.price)
}
is RemoteBackupsSettingsState.BackupState.SubscriptionMismatchMissingGooglePlay -> {
SubscriptionMismatchMissingGooglePlayCard(
state = state,
onLearnMoreClick = contentCallbacks::onLearnMoreAboutLostSubscription,
onRenewClick = contentCallbacks::onRenewLostSubscription
)
}
is RemoteBackupsSettingsState.BackupState.SubscriptionMismatchMissingGooglePlay -> {
SubscriptionMismatchMissingGooglePlayCard(
state = backupState,
onLearnMoreClick = contentCallbacks::onLearnMoreAboutLostSubscription,
onRenewClick = contentCallbacks::onRenewLostSubscription
)
}
RemoteBackupsSettingsState.BackupState.None -> Unit
RemoteBackupsSettingsState.BackupState.None -> Unit
is RemoteBackupsSettingsState.BackupState.WithTypeAndRenewalTime -> {
BackupCard(
backupState = state,
onBackupTypeActionButtonClicked = contentCallbacks::onBackupTypeActionClick
)
}
is RemoteBackupsSettingsState.BackupState.WithTypeAndRenewalTime -> {
BackupCard(
backupState = backupState,
onBackupTypeActionButtonClicked = contentCallbacks::onBackupTypeActionClick
)
}
}
}
@@ -993,14 +992,26 @@ private fun InProgressBackupRow(
Column(
modifier = Modifier.weight(1f)
) {
val backupProgress = getBackupProgress(archiveUploadProgressState)
if (backupProgress.total == 0L) {
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
} else {
LinearProgressIndicator(
modifier = Modifier.fillMaxWidth(),
progress = { backupProgress.progress }
)
when (archiveUploadProgressState.state) {
ArchiveUploadProgressState.State.None -> {
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
}
ArchiveUploadProgressState.State.Export -> {
val progressValue by animateFloatAsState(targetValue = archiveUploadProgressState.frameExportProgress(), animationSpec = tween(durationMillis = 250))
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(
@@ -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
private fun getProgressStateMessage(archiveUploadProgressState: ArchiveUploadProgressState): String {
return when (archiveUploadProgressState.state) {
ArchiveUploadProgressState.State.None -> stringResource(R.string.RemoteBackupsSettingsFragment__processing_backup)
ArchiveUploadProgressState.State.BackingUpMessages -> getBackupPhaseMessage(archiveUploadProgressState)
ArchiveUploadProgressState.State.UploadingMessages -> getUploadingMessages(archiveUploadProgressState)
ArchiveUploadProgressState.State.UploadingAttachments -> getUploadingAttachmentsMessage(archiveUploadProgressState)
ArchiveUploadProgressState.State.Export -> getBackupExportPhaseProgressString(archiveUploadProgressState)
ArchiveUploadProgressState.State.UploadBackupFile, ArchiveUploadProgressState.State.UploadMedia -> getBackupUploadPhaseProgressString(archiveUploadProgressState)
}
}
@Composable
private fun getBackupPhaseMessage(state: ArchiveUploadProgressState): String {
private fun getBackupExportPhaseProgressString(state: ArchiveUploadProgressState): String {
return when (state.backupPhase) {
ArchiveUploadProgressState.BackupPhase.BackupPhaseNone -> stringResource(R.string.RemoteBackupsSettingsFragment__processing_backup)
ArchiveUploadProgressState.BackupPhase.Message -> {
val progress = getBackupProgress(state)
pluralStringResource(
R.plurals.RemoteBackupsSettingsFragment__processing_d_of_d_d_messages,
progress.total.toInt(),
"%,d".format(progress.completed.toInt()),
"%,d".format(progress.total.toInt()),
(progress.progress * 100).toInt()
R.plurals.RemoteBackupsSettingsFragment__processing_messages_progress_text,
state.frameTotalCount.toInt(),
"%,d".format(state.frameExportCount),
"%,d".format(state.frameTotalCount),
(state.frameExportProgress() * 100).toInt()
)
}
@@ -1047,25 +1051,12 @@ private fun getBackupPhaseMessage(state: ArchiveUploadProgressState): String {
}
@Composable
private fun getUploadingMessages(state: ArchiveUploadProgressState): String {
val formattedCompleted = state.completedAttachments.bytes.toUnitString()
val formattedTotal = state.totalAttachments.bytes.toUnitString()
val percent = if (state.totalAttachments == 0L) {
0
} else {
((state.completedAttachments / state.totalAttachments.toFloat()) * 100).toInt()
}
private fun getBackupUploadPhaseProgressString(state: ArchiveUploadProgressState): String {
val formattedTotalBytes = state.uploadBytesTotal.bytes.toUnitString()
val formattedUploadedBytes = state.uploadBytesUploaded.bytes.toUnitString()
val percent = (state.uploadProgress() * 100).toInt()
return stringResource(R.string.RemoteBackupsSettingsFragment__uploading_s_of_s_d, formattedCompleted, formattedTotal, 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)
}
return stringResource(R.string.RemoteBackupsSettingsFragment__uploading_s_of_s_d, formattedUploadedBytes, formattedTotalBytes, percent)
}
@Composable
@@ -1475,70 +1466,82 @@ private fun InProgressRowPreview() {
InProgressBackupRow(archiveUploadProgressState = ArchiveUploadProgressState())
InProgressBackupRow(
archiveUploadProgressState = ArchiveUploadProgressState(
state = ArchiveUploadProgressState.State.BackingUpMessages,
state = ArchiveUploadProgressState.State.Export,
backupPhase = ArchiveUploadProgressState.BackupPhase.BackupPhaseNone
)
)
InProgressBackupRow(
archiveUploadProgressState = ArchiveUploadProgressState(
state = ArchiveUploadProgressState.State.BackingUpMessages,
state = ArchiveUploadProgressState.State.Export,
backupPhase = ArchiveUploadProgressState.BackupPhase.Account
)
)
InProgressBackupRow(
archiveUploadProgressState = ArchiveUploadProgressState(
state = ArchiveUploadProgressState.State.BackingUpMessages,
state = ArchiveUploadProgressState.State.Export,
backupPhase = ArchiveUploadProgressState.BackupPhase.Call
)
)
InProgressBackupRow(
archiveUploadProgressState = ArchiveUploadProgressState(
state = ArchiveUploadProgressState.State.BackingUpMessages,
state = ArchiveUploadProgressState.State.Export,
backupPhase = ArchiveUploadProgressState.BackupPhase.Sticker
)
)
InProgressBackupRow(
archiveUploadProgressState = ArchiveUploadProgressState(
state = ArchiveUploadProgressState.State.BackingUpMessages,
state = ArchiveUploadProgressState.State.Export,
backupPhase = ArchiveUploadProgressState.BackupPhase.Recipient
)
)
InProgressBackupRow(
archiveUploadProgressState = ArchiveUploadProgressState(
state = ArchiveUploadProgressState.State.BackingUpMessages,
state = ArchiveUploadProgressState.State.Export,
backupPhase = ArchiveUploadProgressState.BackupPhase.Thread
)
)
InProgressBackupRow(
archiveUploadProgressState = ArchiveUploadProgressState(
state = ArchiveUploadProgressState.State.BackingUpMessages,
state = ArchiveUploadProgressState.State.Export,
backupPhase = ArchiveUploadProgressState.BackupPhase.Message,
completedAttachments = 1,
totalAttachments = 1
frameExportCount = 1,
frameTotalCount = 1
)
)
InProgressBackupRow(
archiveUploadProgressState = ArchiveUploadProgressState(
state = ArchiveUploadProgressState.State.BackingUpMessages,
state = ArchiveUploadProgressState.State.Export,
backupPhase = ArchiveUploadProgressState.BackupPhase.Message,
completedAttachments = 1000,
totalAttachments = 100_000
frameExportCount = 1000,
frameTotalCount = 100_000
)
)
InProgressBackupRow(
archiveUploadProgressState = ArchiveUploadProgressState(
state = ArchiveUploadProgressState.State.BackingUpMessages,
state = ArchiveUploadProgressState.State.Export,
backupPhase = ArchiveUploadProgressState.BackupPhase.Message,
completedAttachments = 1_000_000,
totalAttachments = 100_000
frameExportCount = 1_000_000,
frameTotalCount = 100_000
)
)
InProgressBackupRow(
archiveUploadProgressState = ArchiveUploadProgressState(
state = ArchiveUploadProgressState.State.UploadingMessages,
state = ArchiveUploadProgressState.State.UploadBackupFile,
backupPhase = ArchiveUploadProgressState.BackupPhase.BackupPhaseNone,
completedAttachments = 1.gibiBytes.inWholeBytes + 100.mebiBytes.inWholeBytes,
totalAttachments = 12.gibiBytes.inWholeBytes
backupFileUploadedBytes = 10.mebiBytes.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
}
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

View File

@@ -25,6 +25,7 @@ import org.signal.core.util.bytes
import org.signal.core.util.logging.Log
import org.signal.core.util.money.FiatMoney
import org.signal.donations.InAppPaymentType
import org.thoughtcrime.securesms.backup.ArchiveUploadProgress
import org.thoughtcrime.securesms.backup.v2.BackupFrequency
import org.thoughtcrime.securesms.backup.v2.BackupRepository
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.RestoreOptimizedMediaJob
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.keyvalue.protos.ArchiveUploadProgressState
import org.thoughtcrime.securesms.service.MessageBackupListener
import java.util.Currency
import kotlin.time.Duration.Companion.seconds
@@ -103,6 +105,20 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
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) {
@@ -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?) {
val tier = SignalStore.backup.latestBackupTier
@@ -307,39 +359,6 @@ 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 fun refreshLocalState() {
}
}

View File

@@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.components.settings.conversation
import android.graphics.Bitmap
import android.graphics.Color
import android.text.TextUtils
import android.widget.Toast
@@ -16,11 +17,14 @@ import org.signal.core.util.withinTransaction
import org.signal.libsignal.zkgroup.profiles.ProfileKey
import org.thoughtcrime.securesms.MainActivity
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.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.database.AttachmentTable
import org.thoughtcrime.securesms.database.MessageType
import org.thoughtcrime.securesms.database.SignalDatabase
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.OutgoingMessage
import org.thoughtcrime.securesms.profiles.AvatarHelper
import org.thoughtcrime.securesms.providers.BlobProvider
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientForeverObserver
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.Util
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.livedata.Store
import java.util.Objects
import kotlin.random.Random
import kotlin.time.Duration.Companion.nanoseconds
import kotlin.time.DurationUnit
@@ -267,40 +275,71 @@ class InternalConversationSettingsFragment : DSLSettingsFragment(
}
)
clickPref(
title = DSLSettingsText.from("Add 1,000 dummy messages"),
summary = DSLSettingsText.from("Just adds 1,000 random messages to the chat. Text-only, nothing complicated."),
onClick = {
MaterialAlertDialogBuilder(requireContext())
.setTitle("Are you sure?")
.setNegativeButton(android.R.string.cancel) { d, _ -> d.dismiss() }
.setPositiveButton(android.R.string.ok) { _, _ ->
val messageCount = 1000
val startTime = System.currentTimeMillis() - messageCount
SignalDatabase.rawDatabase.withinTransaction {
val targetThread = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
for (i in 1..messageCount) {
val time = startTime + i
if (Math.random() > 0.5) {
if (!recipient.isGroup) {
clickPref(
title = DSLSettingsText.from("Add 1,000 dummy messages"),
summary = DSLSettingsText.from("Just adds 1,000 random messages to the chat. Text-only, nothing complicated."),
onClick = {
MaterialAlertDialogBuilder(requireContext())
.setTitle("Are you sure?")
.setNegativeButton(android.R.string.cancel) { d, _ -> d.dismiss() }
.setPositiveButton(android.R.string.ok) { _, _ ->
val messageCount = 1000
val startTime = System.currentTimeMillis() - messageCount
SignalDatabase.rawDatabase.withinTransaction {
val targetThread = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
for (i in 1..messageCount) {
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(
message = OutgoingMessage(threadRecipient = recipient, sentTimeMillis = time, body = "Outgoing: $i"),
message = OutgoingMessage(threadRecipient = recipient, sentTimeMillis = time, body = "Outgoing: $i", attachments = listOf(attachment)),
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()
}
)
Toast.makeText(context, "Done!", Toast.LENGTH_SHORT).show()
}
.show()
}
)
}
if (recipient.isSelf) {
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) {
Util.copyToClipboard(requireContext(), text)
Toast.makeText(requireContext(), "Copied to clipboard", Toast.LENGTH_SHORT).show()