mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-13 21:43:19 +01:00
Compare commits
80 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8809b8f77c | ||
|
|
f8aa3644aa | ||
|
|
e1e41b6f7f | ||
|
|
b1f067536b | ||
|
|
217a6187c2 | ||
|
|
1d1f0c0b3a | ||
|
|
ba3c30f768 | ||
|
|
bc85552ded | ||
|
|
ccf1be2359 | ||
|
|
0d3727f08b | ||
|
|
94b464e37c | ||
|
|
ca2cc722d4 | ||
|
|
2c47cd2422 | ||
|
|
1c9d68a932 | ||
|
|
807d10837b | ||
|
|
6e5c569f7e | ||
|
|
4179592ae7 | ||
|
|
1f40c7ab7e | ||
|
|
89a0541574 | ||
|
|
5294bd8a1a | ||
|
|
2d9c572c01 | ||
|
|
8520108bb2 | ||
|
|
2572dac8a7 | ||
|
|
54b31514ba | ||
|
|
1166b99d01 | ||
|
|
b44cd5d4c4 | ||
|
|
08a8c56d5c | ||
|
|
33645c302b | ||
|
|
a7ac138ea3 | ||
|
|
06b85cc3cb | ||
|
|
662404d335 | ||
|
|
631b51baf2 | ||
|
|
c29d77d4a5 | ||
|
|
d4c1c39179 | ||
|
|
71dd1d9d8b | ||
|
|
3b715bc461 | ||
|
|
712616e569 | ||
|
|
c18cb6a926 | ||
|
|
b975e2ed69 | ||
|
|
b87a060251 | ||
|
|
c493fc1c4c | ||
|
|
e083076e40 | ||
|
|
a5c4c3b54a | ||
|
|
3bcfb5ab61 | ||
|
|
8ce17e3e2d | ||
|
|
460b097a71 | ||
|
|
8e9dc78957 | ||
|
|
1ee5d32322 | ||
|
|
42905b5bb8 | ||
|
|
b8c25a4d78 | ||
|
|
cdbe2c1c71 | ||
|
|
d4f08e6d46 | ||
|
|
8322bf3ecc | ||
|
|
21363f085e | ||
|
|
9903a664d4 | ||
|
|
1a1ddbfa39 | ||
|
|
23bbe704ab | ||
|
|
0dda3d54c9 | ||
|
|
dde1d9b2c8 | ||
|
|
7bb0b513e8 | ||
|
|
2046b44fce | ||
|
|
45c64f825d | ||
|
|
94ed0650dc | ||
|
|
0d390769d4 | ||
|
|
2872020c1f | ||
|
|
8723fd9a24 | ||
|
|
9a9661149b | ||
|
|
5dfbfccc08 | ||
|
|
a344618c63 | ||
|
|
24b93fb517 | ||
|
|
f052b1fd90 | ||
|
|
a234896438 | ||
|
|
bed718347c | ||
|
|
53f2049c48 | ||
|
|
00d425356d | ||
|
|
6c42ce411b | ||
|
|
1833248c96 | ||
|
|
f5b1857866 | ||
|
|
114524adc6 | ||
|
|
47fb0deca4 |
@@ -21,8 +21,8 @@ plugins {
|
||||
|
||||
apply(from = "static-ips.gradle.kts")
|
||||
|
||||
val canonicalVersionCode = 1578
|
||||
val canonicalVersionName = "7.54.1"
|
||||
val canonicalVersionCode = 1580
|
||||
val canonicalVersionName = "7.56.0"
|
||||
val currentHotfixVersion = 0
|
||||
val maxHotfixVersions = 100
|
||||
|
||||
@@ -237,8 +237,8 @@ android {
|
||||
buildConfigField("String", "STRIPE_BASE_URL", "\"https://api.stripe.com/v1\"")
|
||||
buildConfigField("String", "STRIPE_PUBLISHABLE_KEY", "\"pk_live_6cmGZopuTsV8novGgJJW9JpC00vLIgtQ1D\"")
|
||||
buildConfigField("boolean", "TRACING_ENABLED", "false")
|
||||
buildConfigField("boolean", "MESSAGE_BACKUP_RESTORE_ENABLED", "false")
|
||||
buildConfigField("boolean", "LINK_DEVICE_UX_ENABLED", "false")
|
||||
buildConfigField("boolean", "MESSAGE_BACKUP_RESTORE_ENABLED", "true")
|
||||
buildConfigField("boolean", "LINK_DEVICE_UX_ENABLED", "true")
|
||||
|
||||
ndk {
|
||||
abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -126,7 +126,7 @@ class ConversationItemPreviewer {
|
||||
SignalDatabase.threads.getOrCreateThreadIdFor(other),
|
||||
false,
|
||||
null
|
||||
)
|
||||
).messageId
|
||||
|
||||
SignalDatabase.attachments.getAttachmentsForMessage(insert).forEachIndexed { index, attachment ->
|
||||
SignalDatabase.attachments.setTransferProgressPermanentFailure(attachment.attachmentId, insert)
|
||||
|
||||
@@ -346,5 +346,7 @@ class V2ConversationItemShapeTest {
|
||||
override fun onDisplayMediaNoLongerAvailableSheet() = Unit
|
||||
|
||||
override fun onShowUnverifiedProfileSheet(forGroup: Boolean) = Unit
|
||||
|
||||
override fun onUpdateSignalClicked() = Unit
|
||||
}
|
||||
}
|
||||
|
||||
@@ -388,6 +388,7 @@ class AttachmentTableTest {
|
||||
stickerLocator = null,
|
||||
gif = false,
|
||||
quote = false,
|
||||
quoteTargetContentType = null,
|
||||
uuid = UUID.randomUUID(),
|
||||
fileName = null
|
||||
)
|
||||
|
||||
@@ -252,7 +252,6 @@ class AttachmentTableTest_deduping {
|
||||
assertSkipTransform(id2, true)
|
||||
|
||||
assertDoesNotHaveRemoteFields(id2)
|
||||
assertArchiveFieldsMatch(id1, id2)
|
||||
|
||||
upload(id2)
|
||||
|
||||
@@ -671,7 +670,9 @@ class AttachmentTableTest_deduping {
|
||||
caption = null,
|
||||
stickerLocator = null,
|
||||
blurHash = null,
|
||||
uuid = UUID.randomUUID()
|
||||
uuid = UUID.randomUUID(),
|
||||
quote = false,
|
||||
quoteTargetContentType = null
|
||||
)
|
||||
),
|
||||
quoteAttachment = emptyList()
|
||||
@@ -704,7 +705,7 @@ class AttachmentTableTest_deduping {
|
||||
author = Recipient.self().id,
|
||||
text = "Some quote text",
|
||||
isOriginalMissing = false,
|
||||
attachments = listOf(originalAttachment),
|
||||
attachment = originalAttachment,
|
||||
mentions = emptyList(),
|
||||
type = QuoteModel.Type.NORMAL,
|
||||
bodyRanges = null
|
||||
@@ -713,7 +714,7 @@ class AttachmentTableTest_deduping {
|
||||
threadId = threadId,
|
||||
forceSms = false,
|
||||
insertListener = null
|
||||
)
|
||||
).messageId
|
||||
|
||||
val attachments = SignalDatabase.attachments.getAttachmentsForMessage(messageId)
|
||||
return attachments[0].attachmentId
|
||||
|
||||
@@ -51,6 +51,30 @@ class BackupMediaSnapshotTableTest {
|
||||
assertThat(count).isEqualTo(countWithThumbnails)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAnEmptyTable_whenIWriteToTableAndCommitQuotes_thenIExpectFilledTableWithNoThumbnails() {
|
||||
val inputCount = 100
|
||||
|
||||
SignalDatabase.backupMediaSnapshots.writePendingMediaObjects(generateArchiveMediaItemSequence(count = inputCount, quote = true))
|
||||
SignalDatabase.backupMediaSnapshots.commitPendingRows()
|
||||
|
||||
val count = getCountForLatestSnapshot(includeThumbnails = true)
|
||||
|
||||
assertThat(count).isEqualTo(inputCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAnEmptyTable_whenIWriteToTableAndCommitNonMedia_thenIExpectFilledTableWithNoThumbnails() {
|
||||
val inputCount = 100
|
||||
|
||||
SignalDatabase.backupMediaSnapshots.writePendingMediaObjects(generateArchiveMediaItemSequence(count = inputCount, contentType = "text/plain"))
|
||||
SignalDatabase.backupMediaSnapshots.commitPendingRows()
|
||||
|
||||
val count = getCountForLatestSnapshot(includeThumbnails = true)
|
||||
|
||||
assertThat(count).isEqualTo(inputCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAFilledTable_whenIReinsertObjects_thenIExpectUncommittedOverrides() {
|
||||
val initialCount = 100
|
||||
@@ -290,19 +314,21 @@ class BackupMediaSnapshotTableTest {
|
||||
.readToSingleInt(0)
|
||||
}
|
||||
|
||||
private fun generateArchiveMediaItemSequence(count: Int): Sequence<ArchiveMediaItem> {
|
||||
private fun generateArchiveMediaItemSequence(count: Int, quote: Boolean = false, contentType: String = "image/jpeg"): Sequence<ArchiveMediaItem> {
|
||||
return (1..count)
|
||||
.asSequence()
|
||||
.map { createArchiveMediaItem(it) }
|
||||
.map { createArchiveMediaItem(it, quote = quote, contentType = contentType) }
|
||||
}
|
||||
|
||||
private fun createArchiveMediaItem(seed: Int, cdn: Int = 0): ArchiveMediaItem {
|
||||
private fun createArchiveMediaItem(seed: Int, cdn: Int = 0, quote: Boolean = false, contentType: String = "image/jpeg"): ArchiveMediaItem {
|
||||
return ArchiveMediaItem(
|
||||
mediaId = "media_id_$seed",
|
||||
thumbnailMediaId = "thumbnail_media_id_$seed",
|
||||
cdn = cdn,
|
||||
plaintextHash = Util.toByteArray(seed),
|
||||
remoteKey = Util.toByteArray(seed)
|
||||
remoteKey = Util.toByteArray(seed),
|
||||
quote = quote,
|
||||
contentType = contentType
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ object MmsHelper {
|
||||
message: OutgoingMessage,
|
||||
threadId: Long
|
||||
): Long {
|
||||
return SignalDatabase.messages.insertMessageOutbox(message, threadId, false, GroupReceiptTable.STATUS_UNKNOWN, null)
|
||||
return SignalDatabase.messages.insertMessageOutbox(message, threadId, false, GroupReceiptTable.STATUS_UNKNOWN, null).messageId
|
||||
}
|
||||
|
||||
fun insert(
|
||||
|
||||
@@ -19,6 +19,7 @@ object UriAttachmentBuilder {
|
||||
borderless: Boolean = false,
|
||||
videoGif: Boolean = false,
|
||||
quote: Boolean = false,
|
||||
quoteTargetContentType: String? = null,
|
||||
caption: String? = null,
|
||||
stickerLocator: StickerLocator? = null,
|
||||
blurHash: BlurHash? = null,
|
||||
@@ -39,6 +40,7 @@ object UriAttachmentBuilder {
|
||||
borderless = borderless,
|
||||
videoGif = videoGif,
|
||||
quote = quote,
|
||||
quoteTargetContentType = quoteTargetContentType,
|
||||
caption = caption,
|
||||
stickerLocator = stickerLocator,
|
||||
blurHash = blurHash,
|
||||
|
||||
@@ -124,7 +124,7 @@ class EditMessageSyncProcessorTest {
|
||||
bodyRanges = content.dataMessage?.bodyRanges.toBodyRangeList()
|
||||
)
|
||||
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(toRecipient)
|
||||
val originalMessageId = SignalDatabase.messages.insertMessageOutbox(originalTextMessage, threadId, false, null)
|
||||
val originalMessageId = SignalDatabase.messages.insertMessageOutbox(originalTextMessage, threadId, false, null).messageId
|
||||
SignalDatabase.messages.markAsSent(originalMessageId, true)
|
||||
if ((content.dataMessage?.expireTimer ?: 0) > 0) {
|
||||
SignalDatabase.messages.markExpireStarted(originalMessageId, originalTimestamp)
|
||||
@@ -141,7 +141,7 @@ class EditMessageSyncProcessorTest {
|
||||
messageToEdit = originalMessageId
|
||||
)
|
||||
|
||||
val editMessageId = SignalDatabase.messages.insertMessageOutbox(editMessage, threadId, false, null)
|
||||
val editMessageId = SignalDatabase.messages.insertMessageOutbox(editMessage, threadId, false, null).messageId
|
||||
SignalDatabase.messages.markAsSent(editMessageId, true)
|
||||
|
||||
if ((content.dataMessage?.expireTimer ?: 0) > 0) {
|
||||
|
||||
@@ -91,7 +91,7 @@ class MessageHelper(private val harness: SignalActivityRule, var startTime: Long
|
||||
).let { updateMessage?.invoke(it) ?: it }
|
||||
|
||||
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(threadRecipient)
|
||||
val messageId = SignalDatabase.messages.insertMessageOutbox(message, threadId, false, null)
|
||||
val messageId = SignalDatabase.messages.insertMessageOutbox(message, threadId, false, null).messageId
|
||||
|
||||
if (successfulSend) {
|
||||
SignalDatabase.messages.markAsSent(messageId, true)
|
||||
@@ -114,7 +114,7 @@ class MessageHelper(private val harness: SignalActivityRule, var startTime: Long
|
||||
).apply { updateMessage() }
|
||||
|
||||
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(threadRecipient)
|
||||
val messageId = SignalDatabase.messages.insertMessageOutbox(message, threadId, false, null)
|
||||
val messageId = SignalDatabase.messages.insertMessageOutbox(message, threadId, false, null).messageId
|
||||
|
||||
return messageData.copy(messageId = messageId)
|
||||
}
|
||||
@@ -151,7 +151,7 @@ class MessageHelper(private val harness: SignalActivityRule, var startTime: Long
|
||||
val outgoingMessage = OutgoingMessage.groupUpdateMessage(groupRecipient, updateDescription, startTime)
|
||||
|
||||
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(groupRecipient)
|
||||
val messageId = SignalDatabase.messages.insertMessageOutbox(outgoingMessage, threadId, false, null)
|
||||
val messageId = SignalDatabase.messages.insertMessageOutbox(outgoingMessage, threadId, false, null).messageId
|
||||
SignalDatabase.messages.markAsSent(messageId, true)
|
||||
|
||||
return messageData.copy(messageId = messageId)
|
||||
|
||||
@@ -21,6 +21,7 @@ import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.testing.AliceClient
|
||||
import org.thoughtcrime.securesms.testing.BobClient
|
||||
import org.thoughtcrime.securesms.testing.Entry
|
||||
import org.thoughtcrime.securesms.testing.FakeClientHelpers
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
import org.thoughtcrime.securesms.testing.awaitFor
|
||||
import org.whispersystems.signalservice.internal.push.Envelope
|
||||
@@ -54,8 +55,7 @@ class MessageProcessingPerformanceTest {
|
||||
@Before
|
||||
fun setup() {
|
||||
mockkStatic(SealedSenderAccessUtil::class)
|
||||
// TODO reinstate this for libsignal 0.76.1
|
||||
// every { SealedSenderAccessUtil.getCertificateValidator() } returns FakeClientHelpers.noOpCertificateValidator
|
||||
every { SealedSenderAccessUtil.getCertificateValidator() } returns FakeClientHelpers.noOpCertificateValidator
|
||||
|
||||
mockkObject(MessageContentProcessor)
|
||||
every { MessageContentProcessor.create(harness.application) } returns TimingMessageContentProcessor(harness.application)
|
||||
|
||||
@@ -705,7 +705,8 @@ class SyncMessageProcessorTest_synchronizeDeleteForMe {
|
||||
archiveCdn = this.archiveCdn,
|
||||
thumbnailRestoreState = this.thumbnailRestoreState,
|
||||
archiveTransferState = this.archiveTransferState,
|
||||
uuid = uuid
|
||||
uuid = uuid,
|
||||
quoteTargetContentType = this.quoteTargetContentType
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.testing
|
||||
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.signal.core.util.Base64
|
||||
import org.signal.libsignal.metadata.certificate.CertificateValidator
|
||||
import org.signal.libsignal.metadata.certificate.SenderCertificate
|
||||
import org.signal.libsignal.metadata.certificate.ServerCertificate
|
||||
import org.signal.libsignal.protocol.ecc.ECKeyPair
|
||||
@@ -22,10 +23,9 @@ import java.util.UUID
|
||||
|
||||
object FakeClientHelpers {
|
||||
|
||||
// TODO reinstate this for libsignal 0.76.1
|
||||
// val noOpCertificateValidator = object : CertificateValidator(ECKeyPair.generate().publicKey) {
|
||||
// override fun validate(certificate: SenderCertificate, validationTime: Long) = Unit
|
||||
// }
|
||||
val noOpCertificateValidator = object : CertificateValidator(ECKeyPair.generate().publicKey) {
|
||||
override fun validate(certificate: SenderCertificate, validationTime: Long) = Unit
|
||||
}
|
||||
|
||||
fun createCertificateFor(trustRoot: ECKeyPair, uuid: UUID, e164: String, deviceId: Int, identityKey: ECPublicKey, expires: Long): SenderCertificate {
|
||||
val serverKey: ECKeyPair = ECKeyPair.generate()
|
||||
|
||||
@@ -332,5 +332,9 @@ class InternalConversationTestFragment : Fragment(R.layout.conversation_test_fra
|
||||
override fun onShowUnverifiedProfileSheet(forGroup: Boolean) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onUpdateSignalClicked() {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1220,6 +1220,11 @@
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:exported="false"/>
|
||||
|
||||
<service
|
||||
android:name=".service.BackupMediaRestoreService"
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:exported="false"/>
|
||||
|
||||
<service
|
||||
android:name=".gcm.FcmFetchBackgroundService"
|
||||
android:exported="false"/>
|
||||
|
||||
@@ -69,7 +69,7 @@ class DecryptableStreamLocalUriFetcher extends StreamLocalUriFetcher {
|
||||
try {
|
||||
if (PartAuthority.isBlobUri(uri) && BlobProvider.isSingleUseMemoryBlob(uri)) {
|
||||
return PartAuthority.getAttachmentThumbnailStream(context, uri);
|
||||
} else if (isSafeSize(PartAuthority.getAttachmentThumbnailStream(context, uri))) {
|
||||
} else if (isSafeSize(context, uri)) {
|
||||
return PartAuthority.getAttachmentThumbnailStream(context, uri);
|
||||
} else {
|
||||
throw new IOException("File dimensions are too large!");
|
||||
@@ -80,13 +80,15 @@ class DecryptableStreamLocalUriFetcher extends StreamLocalUriFetcher {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isSafeSize(InputStream stream) {
|
||||
private boolean isSafeSize(Context context, Uri uri) throws IOException {
|
||||
try {
|
||||
InputStream stream = PartAuthority.getAttachmentThumbnailStream(context, uri);
|
||||
Pair<Integer, Integer> dimensions = BitmapUtil.getDimensions(stream);
|
||||
long totalPixels = (long) dimensions.first * dimensions.second;
|
||||
return totalPixels < TOTAL_PIXEL_SIZE_LIMIT;
|
||||
} catch (BitmapDecodingException e) {
|
||||
return false;
|
||||
Long size = PartAuthority.getAttachmentSize(context, uri);
|
||||
return size != null && size < GlideStreamConfig.getMarkReadLimitBytes();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,9 +9,11 @@ import org.thoughtcrime.securesms.dependencies.AppDependencies;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||
import org.thoughtcrime.securesms.jobs.DeleteAbandonedAttachmentsJob;
|
||||
import org.thoughtcrime.securesms.jobs.EmojiSearchIndexDownloadJob;
|
||||
import org.thoughtcrime.securesms.jobs.QuoteThumbnailBackfillJob;
|
||||
import org.thoughtcrime.securesms.jobs.StickerPackDownloadJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.migrations.ApplicationMigrations;
|
||||
import org.thoughtcrime.securesms.migrations.QuoteThumbnailBackfillMigrationJob;
|
||||
import org.thoughtcrime.securesms.stickers.BlessedPacks;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
@@ -53,6 +55,8 @@ public final class AppInitialization {
|
||||
SignalStore.onPostBackupRestore();
|
||||
SignalStore.onFirstEverAppLaunch();
|
||||
SignalStore.onboarding().clearAll();
|
||||
SignalStore.settings().setPassphraseDisabled(true);
|
||||
SignalStore.notificationProfile().setHasSeenTooltip(true);
|
||||
TextSecurePreferences.onPostBackupRestore(context);
|
||||
SignalStore.settings().setPassphraseDisabled(true);
|
||||
AppDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.ZOZO.getPackId(), BlessedPacks.ZOZO.getPackKey(), false));
|
||||
@@ -62,6 +66,12 @@ public final class AppInitialization {
|
||||
AppDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_FACES.getPackId(), BlessedPacks.SWOON_FACES.getPackKey()));
|
||||
EmojiSearchIndexDownloadJob.scheduleImmediately();
|
||||
DeleteAbandonedAttachmentsJob.enqueue();
|
||||
|
||||
if (SignalStore.misc().startedQuoteThumbnailMigration()) {
|
||||
AppDependencies.getJobManager().add(new QuoteThumbnailBackfillJob());
|
||||
} else {
|
||||
AppDependencies.getJobManager().add(new QuoteThumbnailBackfillMigrationJob());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -205,6 +205,7 @@ public class ApplicationContext extends Application implements AppForegroundObse
|
||||
.addNonBlocking(AppDependencies::getBillingApi)
|
||||
.addNonBlocking(this::ensureProfileUploaded)
|
||||
.addNonBlocking(() -> AppDependencies.getExpireStoriesManager().scheduleIfNecessary())
|
||||
.addNonBlocking(BackupRepository::maybeFixAnyDanglingUploadProgress)
|
||||
.addPostRender(() -> AppDependencies.getDeletedCallEventManager().scheduleIfNecessary())
|
||||
.addPostRender(() -> RateLimitUtil.retryAllRateLimitedMessages(this))
|
||||
.addPostRender(this::initializeExpiringMessageManager)
|
||||
|
||||
@@ -142,5 +142,6 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
|
||||
void onPaymentTombstoneClicked();
|
||||
void onDisplayMediaNoLongerAvailableSheet();
|
||||
void onShowUnverifiedProfileSheet(boolean forGroup);
|
||||
void onUpdateSignalClicked();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.BoxWithConstraintsScope
|
||||
@@ -36,10 +37,8 @@ import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SnackbarDuration
|
||||
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
|
||||
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
|
||||
import androidx.compose.material3.adaptive.layout.calculatePaneScaffoldDirective
|
||||
import androidx.compose.material3.adaptive.layout.rememberPaneExpansionState
|
||||
import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator
|
||||
import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -63,6 +62,8 @@ import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.signal.core.ui.compose.theme.SignalTheme
|
||||
@@ -70,6 +71,7 @@ import org.signal.core.util.concurrent.LifecycleDisposable
|
||||
import org.signal.core.util.getSerializableCompat
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.donations.StripeApi
|
||||
import org.thoughtcrime.securesms.backup.v2.ArchiveRestoreProgress
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.verify.VerifyBackupKeyActivity
|
||||
import org.thoughtcrime.securesms.calls.YouAreAlreadyInACallSnackbar.show
|
||||
import org.thoughtcrime.securesms.calls.log.CallLogFilter
|
||||
@@ -127,6 +129,7 @@ import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
|
||||
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfiles
|
||||
import org.thoughtcrime.securesms.permissions.Permissions
|
||||
import org.thoughtcrime.securesms.profiles.manage.UsernameEditFragment
|
||||
import org.thoughtcrime.securesms.service.BackupMediaRestoreService
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService
|
||||
import org.thoughtcrime.securesms.stories.Stories
|
||||
import org.thoughtcrime.securesms.stories.landing.StoriesLandingFragment
|
||||
@@ -143,8 +146,10 @@ import org.thoughtcrime.securesms.util.SplashScreenUtil
|
||||
import org.thoughtcrime.securesms.util.TopToastPopup
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.thoughtcrime.securesms.util.viewModel
|
||||
import org.thoughtcrime.securesms.window.AppPaneDragHandle
|
||||
import org.thoughtcrime.securesms.window.AppScaffold
|
||||
import org.thoughtcrime.securesms.window.WindowSizeClass
|
||||
import org.thoughtcrime.securesms.window.rememberAppScaffoldNavigator
|
||||
import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState
|
||||
|
||||
class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner, MainNavigator.NavigatorProvider, Material3OnScrollHelperBinder, ConversationListFragment.Callback, CallLogFragment.Callback {
|
||||
@@ -253,6 +258,20 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
ArchiveRestoreProgress
|
||||
.stateFlow
|
||||
.distinctUntilChangedBy { it.needRestoreMediaService() }
|
||||
.filter { it.needRestoreMediaService() }
|
||||
.collect {
|
||||
Log.i(TAG, "Still restoring media, launching a service. Remaining restoration size: ${it.remainingRestoreSize} out of ${it.totalRestoreSize} ")
|
||||
BackupMediaRestoreService.resetTimeout()
|
||||
BackupMediaRestoreService.start(this@MainActivity, resources.getString(R.string.BackupStatus__restoring_media))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val callback = object : OnBackPressedCallback(toolbarViewModel.state.value.mode == MainToolbarMode.ACTION_MODE) {
|
||||
@@ -310,9 +329,12 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
|
||||
|
||||
MainContainer {
|
||||
val wrappedNavigator = rememberNavigator(windowSizeClass, contentLayoutData, maxWidth)
|
||||
val paneExpansionState = rememberPaneExpansionState()
|
||||
val mutableInteractionSource = remember { MutableInteractionSource() }
|
||||
|
||||
AppScaffold(
|
||||
navigator = wrappedNavigator,
|
||||
paneExpansionState = paneExpansionState,
|
||||
bottomNavContent = {
|
||||
if (isNavigationVisible) {
|
||||
Column(
|
||||
@@ -371,6 +393,7 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
|
||||
MainNavigationListLocation.ARCHIVE -> {
|
||||
val state = key(destination) { rememberFragmentState() }
|
||||
AndroidFragment(
|
||||
@@ -379,6 +402,7 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
|
||||
MainNavigationListLocation.CALLS -> {
|
||||
val state = key(destination) { rememberFragmentState() }
|
||||
AndroidFragment(
|
||||
@@ -387,6 +411,7 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
|
||||
MainNavigationListLocation.STORIES -> {
|
||||
val state = key(destination) { rememberFragmentState() }
|
||||
AndroidFragment(
|
||||
@@ -440,7 +465,12 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
|
||||
}
|
||||
},
|
||||
paneExpansionDragHandle = if (contentLayoutData.hasDragHandle()) {
|
||||
{ }
|
||||
{
|
||||
AppPaneDragHandle(
|
||||
paneExpansionState = paneExpansionState,
|
||||
mutableInteractionSource = mutableInteractionSource
|
||||
)
|
||||
}
|
||||
} else null
|
||||
)
|
||||
}
|
||||
@@ -480,14 +510,10 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
|
||||
contentLayoutData: MainContentLayoutData,
|
||||
maxWidth: Dp
|
||||
): ThreePaneScaffoldNavigator<Any> {
|
||||
val scaffoldNavigator = rememberListDetailPaneScaffoldNavigator<Any>(
|
||||
scaffoldDirective = calculatePaneScaffoldDirective(
|
||||
currentWindowAdaptiveInfo()
|
||||
).copy(
|
||||
maxHorizontalPartitions = if (windowSizeClass.isSplitPane()) 2 else 1,
|
||||
horizontalPartitionSpacerSize = contentLayoutData.partitionWidth,
|
||||
defaultPanePreferredWidth = contentLayoutData.rememberDefaultPanePreferredWidth(maxWidth)
|
||||
)
|
||||
val scaffoldNavigator = rememberAppScaffoldNavigator(
|
||||
isSplitPane = windowSizeClass.isSplitPane(),
|
||||
horizontalPartitionSpacerSize = contentLayoutData.partitionWidth,
|
||||
defaultPanePreferredWidth = contentLayoutData.rememberDefaultPanePreferredWidth(maxWidth)
|
||||
)
|
||||
|
||||
val coroutine = rememberCoroutineScope()
|
||||
@@ -498,6 +524,7 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
|
||||
is MainNavigationDetailLocation.Conversation -> {
|
||||
startActivity(detailLocation.intent)
|
||||
}
|
||||
|
||||
MainNavigationDetailLocation.Empty -> Unit
|
||||
}
|
||||
}
|
||||
@@ -516,7 +543,9 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
|
||||
}
|
||||
|
||||
val modifier = if (windowSizeClass.isSplitPane()) {
|
||||
Modifier.systemBarsPadding().displayCutoutPadding()
|
||||
Modifier
|
||||
.systemBarsPadding()
|
||||
.displayCutoutPadding()
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
|
||||
@@ -45,11 +45,13 @@ class ArchivedAttachment : Attachment {
|
||||
stickerLocator: StickerLocator?,
|
||||
gif: Boolean,
|
||||
quote: Boolean,
|
||||
quoteTargetContentType: String?,
|
||||
uuid: UUID?,
|
||||
fileName: String?
|
||||
) : super(
|
||||
contentType = contentType ?: "",
|
||||
quote = quote,
|
||||
quoteTargetContentType = quoteTargetContentType,
|
||||
transferState = AttachmentTable.TRANSFER_NEEDS_RESTORE,
|
||||
size = size,
|
||||
fileName = fileName,
|
||||
|
||||
@@ -59,6 +59,8 @@ abstract class Attachment(
|
||||
@JvmField
|
||||
val quote: Boolean,
|
||||
@JvmField
|
||||
val quoteTargetContentType: String?,
|
||||
@JvmField
|
||||
val uploadTimestamp: Long,
|
||||
@JvmField
|
||||
val caption: String?,
|
||||
@@ -98,6 +100,7 @@ abstract class Attachment(
|
||||
height = parcel.readInt(),
|
||||
incrementalMacChunkSize = parcel.readInt(),
|
||||
quote = ParcelUtil.readBoolean(parcel),
|
||||
quoteTargetContentType = parcel.readString(),
|
||||
uploadTimestamp = parcel.readLong(),
|
||||
caption = parcel.readString(),
|
||||
stickerLocator = ParcelCompat.readParcelable(parcel, StickerLocator::class.java.classLoader, StickerLocator::class.java),
|
||||
@@ -126,6 +129,7 @@ abstract class Attachment(
|
||||
dest.writeInt(height)
|
||||
dest.writeInt(incrementalMacChunkSize)
|
||||
ParcelUtil.writeBoolean(dest, quote)
|
||||
dest.writeString(quoteTargetContentType)
|
||||
dest.writeLong(uploadTimestamp)
|
||||
dest.writeString(caption)
|
||||
dest.writeParcelable(stickerLocator, 0)
|
||||
|
||||
@@ -75,7 +75,8 @@ class DatabaseAttachment : Attachment {
|
||||
archiveCdn: Int?,
|
||||
thumbnailRestoreState: AttachmentTable.ThumbnailRestoreState,
|
||||
archiveTransferState: AttachmentTable.ArchiveTransferState,
|
||||
uuid: UUID?
|
||||
uuid: UUID?,
|
||||
quoteTargetContentType: String?
|
||||
) : super(
|
||||
contentType = contentType,
|
||||
transferState = transferProgress,
|
||||
@@ -93,6 +94,7 @@ class DatabaseAttachment : Attachment {
|
||||
height = height,
|
||||
incrementalMacChunkSize = incrementalMacChunkSize,
|
||||
quote = quote,
|
||||
quoteTargetContentType = quoteTargetContentType,
|
||||
uploadTimestamp = uploadTimestamp,
|
||||
caption = caption,
|
||||
stickerLocator = stickerLocator,
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.attachments
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Parcel
|
||||
import androidx.core.os.ParcelCompat
|
||||
import org.thoughtcrime.securesms.database.AttachmentTable
|
||||
import org.thoughtcrime.securesms.database.model.StickerRecord
|
||||
import org.thoughtcrime.securesms.mms.StickerSlide
|
||||
import org.thoughtcrime.securesms.stickers.StickerLocator
|
||||
import java.security.SecureRandom
|
||||
|
||||
/**
|
||||
* An incoming sticker that is already available locally via an installed sticker pack.
|
||||
*/
|
||||
class LocalStickerAttachment : Attachment {
|
||||
|
||||
constructor(
|
||||
stickerRecord: StickerRecord,
|
||||
stickerLocator: StickerLocator
|
||||
) : super(
|
||||
contentType = stickerRecord.contentType,
|
||||
transferState = AttachmentTable.TRANSFER_PROGRESS_DONE,
|
||||
size = stickerRecord.size,
|
||||
fileName = null,
|
||||
cdn = Cdn.CDN_0,
|
||||
remoteLocation = null,
|
||||
remoteKey = null,
|
||||
remoteDigest = null,
|
||||
incrementalDigest = null,
|
||||
fastPreflightId = SecureRandom().nextLong().toString(),
|
||||
voiceNote = false,
|
||||
borderless = false,
|
||||
videoGif = false,
|
||||
width = StickerSlide.WIDTH,
|
||||
height = StickerSlide.HEIGHT,
|
||||
incrementalMacChunkSize = 0,
|
||||
quote = false,
|
||||
quoteTargetContentType = null,
|
||||
uploadTimestamp = 0,
|
||||
caption = null,
|
||||
stickerLocator = stickerLocator,
|
||||
blurHash = null,
|
||||
audioHash = null,
|
||||
transformProperties = null,
|
||||
uuid = null
|
||||
) {
|
||||
uri = stickerRecord.uri
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
constructor(parcel: Parcel) : super(parcel) {
|
||||
uri = ParcelCompat.readParcelable(parcel, Uri::class.java.classLoader, Uri::class.java)!!
|
||||
}
|
||||
|
||||
override val uri: Uri
|
||||
override val publicUri: Uri? = null
|
||||
override val thumbnailUri: Uri? = null
|
||||
|
||||
val packId: String = stickerLocator!!.packId
|
||||
val stickerId: Int = stickerLocator!!.stickerId
|
||||
|
||||
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||
super.writeToParcel(dest, flags)
|
||||
dest.writeParcelable(uri, 0)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
return other != null && other is LocalStickerAttachment && other.uri == uri
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return uri.hashCode()
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,9 @@ class PointerAttachment : Attachment {
|
||||
caption: String?,
|
||||
stickerLocator: StickerLocator?,
|
||||
blurHash: BlurHash?,
|
||||
uuid: UUID?
|
||||
uuid: UUID?,
|
||||
quote: Boolean,
|
||||
quoteTargetContentType: String? = null
|
||||
) : super(
|
||||
contentType = contentType,
|
||||
transferState = transferState,
|
||||
@@ -56,7 +58,8 @@ class PointerAttachment : Attachment {
|
||||
width = width,
|
||||
height = height,
|
||||
incrementalMacChunkSize = incrementalMacChunkSize,
|
||||
quote = false,
|
||||
quote = quote,
|
||||
quoteTargetContentType = quoteTargetContentType,
|
||||
uploadTimestamp = uploadTimestamp,
|
||||
caption = caption,
|
||||
stickerLocator = stickerLocator,
|
||||
@@ -91,7 +94,9 @@ class PointerAttachment : Attachment {
|
||||
pointer: Optional<SignalServiceAttachment>,
|
||||
stickerLocator: StickerLocator? = null,
|
||||
fastPreflightId: String? = null,
|
||||
transferState: Int = AttachmentTable.TRANSFER_PROGRESS_PENDING
|
||||
transferState: Int = AttachmentTable.TRANSFER_PROGRESS_PENDING,
|
||||
quote: Boolean = false,
|
||||
quoteTargetContentType: String? = null
|
||||
): Optional<Attachment> {
|
||||
if (!pointer.isPresent || !pointer.get().isPointer()) {
|
||||
return Optional.empty()
|
||||
@@ -122,7 +127,9 @@ class PointerAttachment : Attachment {
|
||||
caption = pointer.get().asPointer().caption.orElse(null),
|
||||
stickerLocator = stickerLocator,
|
||||
blurHash = BlurHash.parseOrNull(pointer.get().asPointer().blurHash.orElse(null)),
|
||||
uuid = pointer.get().asPointer().uuid
|
||||
uuid = pointer.get().asPointer().uuid,
|
||||
quote = quote,
|
||||
quoteTargetContentType = quoteTargetContentType
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -140,7 +147,9 @@ class PointerAttachment : Attachment {
|
||||
|
||||
return Optional.of(
|
||||
PointerAttachment(
|
||||
contentType = quotedAttachment.contentType!!,
|
||||
quote = true,
|
||||
contentType = quotedAttachment.thumbnail?.contentType,
|
||||
quoteTargetContentType = quotedAttachment.contentType!!,
|
||||
transferState = AttachmentTable.TRANSFER_PROGRESS_PENDING,
|
||||
size = (if (thumbnail != null) thumbnail.asPointer().size.orElse(0) else 0).toLong(),
|
||||
fileName = quotedAttachment.fileName,
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.os.Parcel
|
||||
import org.thoughtcrime.securesms.blurhash.BlurHash
|
||||
import org.thoughtcrime.securesms.database.AttachmentTable
|
||||
import org.thoughtcrime.securesms.stickers.StickerLocator
|
||||
import org.thoughtcrime.securesms.util.MediaUtil
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
@@ -14,9 +15,21 @@ import java.util.UUID
|
||||
* quote them and know their contentType even though the media has been deleted.
|
||||
*/
|
||||
class TombstoneAttachment : Attachment {
|
||||
constructor(contentType: String?, quote: Boolean) : super(
|
||||
|
||||
companion object {
|
||||
fun forQuote(): TombstoneAttachment {
|
||||
return TombstoneAttachment(contentType = null, quote = true, quoteTargetContentType = MediaUtil.VIEW_ONCE)
|
||||
}
|
||||
|
||||
fun forNonQuote(contentType: String?): TombstoneAttachment {
|
||||
return TombstoneAttachment(contentType = contentType, quote = false, quoteTargetContentType = null)
|
||||
}
|
||||
}
|
||||
|
||||
constructor(contentType: String?, quote: Boolean, quoteTargetContentType: String?) : super(
|
||||
contentType = contentType,
|
||||
quote = quote,
|
||||
quoteTargetContentType = quoteTargetContentType,
|
||||
transferState = AttachmentTable.TRANSFER_PROGRESS_DONE,
|
||||
size = 0,
|
||||
fileName = null,
|
||||
@@ -55,10 +68,12 @@ class TombstoneAttachment : Attachment {
|
||||
gif: Boolean = false,
|
||||
stickerLocator: StickerLocator? = null,
|
||||
quote: Boolean,
|
||||
quoteTargetContentType: String?,
|
||||
uuid: UUID?
|
||||
) : super(
|
||||
contentType = contentType ?: "",
|
||||
quote = quote,
|
||||
quoteTargetContentType = quoteTargetContentType,
|
||||
transferState = AttachmentTable.TRANSFER_PROGRESS_PERMANENT_FAILURE,
|
||||
size = 0,
|
||||
fileName = fileName,
|
||||
|
||||
@@ -22,6 +22,7 @@ class UriAttachment : Attachment {
|
||||
borderless: Boolean,
|
||||
videoGif: Boolean,
|
||||
quote: Boolean,
|
||||
quoteTargetContentType: String?,
|
||||
caption: String?,
|
||||
stickerLocator: StickerLocator?,
|
||||
blurHash: BlurHash?,
|
||||
@@ -40,6 +41,7 @@ class UriAttachment : Attachment {
|
||||
borderless = borderless,
|
||||
videoGif = videoGif,
|
||||
quote = quote,
|
||||
quoteTargetContentType = quoteTargetContentType,
|
||||
caption = caption,
|
||||
stickerLocator = stickerLocator,
|
||||
blurHash = blurHash,
|
||||
@@ -61,6 +63,7 @@ class UriAttachment : Attachment {
|
||||
borderless: Boolean,
|
||||
videoGif: Boolean,
|
||||
quote: Boolean,
|
||||
quoteTargetContentType: String?,
|
||||
caption: String?,
|
||||
stickerLocator: StickerLocator?,
|
||||
blurHash: BlurHash?,
|
||||
@@ -85,6 +88,7 @@ class UriAttachment : Attachment {
|
||||
height = height,
|
||||
incrementalMacChunkSize = 0,
|
||||
quote = quote,
|
||||
quoteTargetContentType = quoteTargetContentType,
|
||||
uploadTimestamp = 0,
|
||||
caption = caption,
|
||||
stickerLocator = stickerLocator,
|
||||
|
||||
@@ -30,6 +30,7 @@ class WallpaperAttachment() : Attachment(
|
||||
height = 0,
|
||||
incrementalMacChunkSize = 0,
|
||||
quote = false,
|
||||
quoteTargetContentType = null,
|
||||
uploadTimestamp = 0,
|
||||
caption = null,
|
||||
stickerLocator = null,
|
||||
|
||||
@@ -7,12 +7,16 @@ package org.thoughtcrime.securesms.backup
|
||||
|
||||
import org.signal.core.util.LongSerializer
|
||||
|
||||
enum class RestoreState(val id: Int, val inProgress: Boolean) {
|
||||
FAILED(-1, false),
|
||||
enum class RestoreState(private val id: Int, val inProgress: Boolean) {
|
||||
NONE(0, false),
|
||||
PENDING(1, true),
|
||||
RESTORING_DB(2, true),
|
||||
RESTORING_MEDIA(3, true);
|
||||
CALCULATING_MEDIA(4, true),
|
||||
RESTORING_MEDIA(3, true),
|
||||
CANCELING_MEDIA(5, true);
|
||||
|
||||
val isMediaRestoreOperation: Boolean
|
||||
get() = this == CALCULATING_MEDIA || this == RESTORING_MEDIA || this == CANCELING_MEDIA
|
||||
|
||||
companion object {
|
||||
val serializer: LongSerializer<RestoreState> = Serializer()
|
||||
@@ -23,14 +27,8 @@ enum class RestoreState(val id: Int, val inProgress: Boolean) {
|
||||
return data.id.toLong()
|
||||
}
|
||||
|
||||
override fun deserialize(data: Long): RestoreState {
|
||||
return when (data.toInt()) {
|
||||
FAILED.id -> FAILED
|
||||
PENDING.id -> PENDING
|
||||
RESTORING_DB.id -> RESTORING_DB
|
||||
RESTORING_MEDIA.id -> RESTORING_MEDIA
|
||||
else -> NONE
|
||||
}
|
||||
override fun deserialize(input: Long): RestoreState {
|
||||
return entries.firstOrNull { it.id == input.toInt() } ?: throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,22 +5,239 @@
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.net.ConnectivityManager
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import org.signal.core.util.bytes
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.throttleLatest
|
||||
import org.thoughtcrime.securesms.BuildConfig
|
||||
import org.thoughtcrime.securesms.attachments.AttachmentId
|
||||
import org.thoughtcrime.securesms.backup.RestoreState
|
||||
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 java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import kotlin.concurrent.withLock
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import kotlin.time.DurationUnit
|
||||
|
||||
/**
|
||||
* A class for tracking restore progress, largely just for debugging purposes. It keeps no state on disk, and is therefore only useful for testing.
|
||||
* A class for tracking restore progress as a whole, but with a primary focus on managing media restore.
|
||||
*
|
||||
* Also provides helpful debugging information for attachment download speeds.
|
||||
*/
|
||||
object ArchiveRestoreProgress {
|
||||
private val TAG = Log.tag(ArchiveRestoreProgress::class.java)
|
||||
|
||||
private var listenersRegistered = false
|
||||
private val listenerLock = ReentrantLock()
|
||||
|
||||
private val attachmentObserver = DatabaseObserver.Observer {
|
||||
update()
|
||||
}
|
||||
|
||||
private val networkChangeReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
update()
|
||||
}
|
||||
}
|
||||
|
||||
private val store = MutableStateFlow(
|
||||
ArchiveRestoreProgressState(
|
||||
restoreState = SignalStore.backup.restoreState,
|
||||
remainingRestoreSize = SignalStore.backup.totalRestorableAttachmentSize.bytes,
|
||||
totalRestoreSize = SignalStore.backup.totalRestorableAttachmentSize.bytes,
|
||||
hasActivelyRestoredThisRun = SignalStore.backup.totalRestorableAttachmentSize > 0,
|
||||
totalToRestoreThisRun = SignalStore.backup.totalRestorableAttachmentSize.bytes,
|
||||
restoreStatus = ArchiveRestoreProgressState.RestoreStatus.NONE
|
||||
)
|
||||
)
|
||||
|
||||
val state: ArchiveRestoreProgressState
|
||||
get() = store.value
|
||||
|
||||
val stateFlow: Flow<ArchiveRestoreProgressState> = store
|
||||
.throttleLatest(1.seconds)
|
||||
.distinctUntilChanged()
|
||||
.flowOn(Dispatchers.IO)
|
||||
|
||||
init {
|
||||
SignalExecutors.BOUNDED.execute { update() }
|
||||
}
|
||||
|
||||
fun onRestorePending() {
|
||||
Log.i(TAG, "onRestorePending")
|
||||
SignalStore.backup.restoreState = RestoreState.PENDING
|
||||
update()
|
||||
}
|
||||
|
||||
fun onStartMediaRestore() {
|
||||
Log.i(TAG, "onStartMediaRestore")
|
||||
SignalStore.backup.restoreState = RestoreState.CALCULATING_MEDIA
|
||||
SignalStore.backup.totalRestorableAttachmentSize = SignalDatabase.attachments.getRemainingRestorableAttachmentSize()
|
||||
update()
|
||||
}
|
||||
|
||||
fun onRestoringMedia() {
|
||||
Log.i(TAG, "onRestoringMedia")
|
||||
SignalStore.backup.restoreState = RestoreState.RESTORING_MEDIA
|
||||
SignalStore.backup.totalRestorableAttachmentSize = SignalDatabase.attachments.getRemainingRestorableAttachmentSize()
|
||||
update()
|
||||
}
|
||||
|
||||
fun onRestoringDb() {
|
||||
Log.i(TAG, "onRestoringDb")
|
||||
SignalStore.backup.restoreState = RestoreState.RESTORING_DB
|
||||
update()
|
||||
}
|
||||
|
||||
fun onCancelMediaRestore() {
|
||||
Log.i(TAG, "onCancelMediaRestore")
|
||||
SignalStore.backup.restoreState = RestoreState.CANCELING_MEDIA
|
||||
update()
|
||||
}
|
||||
|
||||
fun allMediaRestored() {
|
||||
val shouldUpdate = if (SignalStore.backup.restoreState == RestoreState.CANCELING_MEDIA) {
|
||||
Log.i(TAG, "allMediaCanceled")
|
||||
store.update { state ->
|
||||
if (state.restoreState == RestoreState.CANCELING_MEDIA) {
|
||||
state.copy(
|
||||
hasActivelyRestoredThisRun = false,
|
||||
totalToRestoreThisRun = 0.bytes
|
||||
)
|
||||
} else {
|
||||
state
|
||||
}
|
||||
}
|
||||
true
|
||||
} else if (SignalStore.backup.restoreState != RestoreState.NONE) {
|
||||
Log.i(TAG, "allMediaRestored")
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
if (shouldUpdate) {
|
||||
SignalStore.backup.totalRestorableAttachmentSize = 0
|
||||
SignalStore.backup.restoreState = RestoreState.NONE
|
||||
update()
|
||||
onProcessEnd()
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun forceUpdate() {
|
||||
update()
|
||||
}
|
||||
|
||||
fun clearFinishedStatus() {
|
||||
store.update { state ->
|
||||
if (state.restoreStatus == ArchiveRestoreProgressState.RestoreStatus.FINISHED) {
|
||||
state.copy(
|
||||
restoreStatus = ArchiveRestoreProgressState.RestoreStatus.NONE,
|
||||
hasActivelyRestoredThisRun = false,
|
||||
totalToRestoreThisRun = 0.bytes
|
||||
)
|
||||
} else {
|
||||
state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun update() {
|
||||
store.update { state ->
|
||||
val remainingRestoreSize = SignalDatabase.attachments.getRemainingRestorableAttachmentSize().bytes
|
||||
var restoreState = SignalStore.backup.restoreState
|
||||
|
||||
val status = when {
|
||||
!WifiConstraint.isMet(AppDependencies.application) && !SignalStore.backup.restoreWithCellular -> ArchiveRestoreProgressState.RestoreStatus.WAITING_FOR_WIFI
|
||||
!NetworkConstraint.isMet(AppDependencies.application) -> ArchiveRestoreProgressState.RestoreStatus.WAITING_FOR_INTERNET
|
||||
!BatteryNotLowConstraint.isMet() -> ArchiveRestoreProgressState.RestoreStatus.LOW_BATTERY
|
||||
restoreState == RestoreState.NONE -> if (state.hasActivelyRestoredThisRun) ArchiveRestoreProgressState.RestoreStatus.FINISHED else ArchiveRestoreProgressState.RestoreStatus.NONE
|
||||
else -> {
|
||||
val availableBytes = SignalStore.backup.spaceAvailableOnDiskBytes
|
||||
|
||||
if (availableBytes > -1L && remainingRestoreSize > availableBytes.bytes) {
|
||||
ArchiveRestoreProgressState.RestoreStatus.NOT_ENOUGH_DISK_SPACE
|
||||
} else {
|
||||
ArchiveRestoreProgressState.RestoreStatus.RESTORING
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (restoreState.isMediaRestoreOperation) {
|
||||
if (remainingRestoreSize == 0.bytes && SignalStore.backup.totalRestorableAttachmentSize == 0L) {
|
||||
restoreState = RestoreState.NONE
|
||||
SignalStore.backup.restoreState = restoreState
|
||||
} else {
|
||||
registerUpdateListeners()
|
||||
}
|
||||
} else {
|
||||
unregisterUpdateListeners()
|
||||
}
|
||||
|
||||
val totalRestoreSize = SignalStore.backup.totalRestorableAttachmentSize.bytes
|
||||
|
||||
state.copy(
|
||||
restoreState = restoreState,
|
||||
remainingRestoreSize = remainingRestoreSize,
|
||||
restoreStatus = status,
|
||||
totalRestoreSize = totalRestoreSize,
|
||||
hasActivelyRestoredThisRun = state.hasActivelyRestoredThisRun || SignalStore.backup.totalRestorableAttachmentSize > 0,
|
||||
totalToRestoreThisRun = if (totalRestoreSize > 0.bytes) totalRestoreSize else state.totalToRestoreThisRun
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun registerUpdateListeners() {
|
||||
if (!listenersRegistered) {
|
||||
listenerLock.withLock {
|
||||
if (!listenersRegistered) {
|
||||
Log.i(TAG, "Registering progress related listeners")
|
||||
AppDependencies.databaseObserver.registerAttachmentUpdatedObserver(attachmentObserver)
|
||||
AppDependencies.application.registerReceiver(networkChangeReceiver, IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION))
|
||||
AppDependencies.application.registerReceiver(networkChangeReceiver, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
|
||||
listenersRegistered = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun unregisterUpdateListeners() {
|
||||
if (listenersRegistered) {
|
||||
listenerLock.withLock {
|
||||
if (listenersRegistered) {
|
||||
Log.i(TAG, "Unregistering listeners")
|
||||
AppDependencies.databaseObserver.unregisterObserver(attachmentObserver)
|
||||
AppDependencies.application.safeUnregisterReceiver(networkChangeReceiver)
|
||||
listenersRegistered = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//region Attachment Debug
|
||||
|
||||
private var debugAttachmentStartTime: Long = 0
|
||||
private val debugTotalAttachments: AtomicInteger = AtomicInteger(0)
|
||||
private val debugTotalBytes: AtomicLong = AtomicLong(0)
|
||||
@@ -53,7 +270,7 @@ object ArchiveRestoreProgress {
|
||||
}
|
||||
}
|
||||
|
||||
fun onProcessEnd() {
|
||||
private fun onProcessEnd() {
|
||||
if (debugAttachmentStartTime <= 0 || debugTotalAttachments.get() <= 0 || debugTotalBytes.get() <= 0) {
|
||||
Log.w(TAG, "Insufficient data to print debug stats.")
|
||||
return
|
||||
@@ -84,4 +301,6 @@ object ArchiveRestoreProgress {
|
||||
return "Duration=${System.currentTimeMillis() - startTimeMs}ms, TotalBytes=$totalBytes (${totalBytes.bytes.toUnitString()}), NetworkRate=$networkBytesPerSecond bytes/sec (${networkBytesPerSecond.bytes.toUnitString()}/sec), DiskRate=$diskBytesPerSecond bytes/sec (${diskBytesPerSecond.bytes.toUnitString()}/sec)"
|
||||
}
|
||||
}
|
||||
|
||||
//endregion Attachment Debug
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2
|
||||
|
||||
import org.signal.core.util.ByteSize
|
||||
import org.signal.core.util.bytes
|
||||
import org.thoughtcrime.securesms.backup.RestoreState
|
||||
|
||||
/**
|
||||
* In-memory view of the current state of an attachment restore process.
|
||||
*/
|
||||
data class ArchiveRestoreProgressState(
|
||||
val restoreState: RestoreState,
|
||||
val remainingRestoreSize: ByteSize,
|
||||
val totalRestoreSize: ByteSize,
|
||||
val hasActivelyRestoredThisRun: Boolean = false,
|
||||
val totalToRestoreThisRun: ByteSize = 0.bytes,
|
||||
val restoreStatus: RestoreStatus
|
||||
) {
|
||||
val completedRestoredSize = totalRestoreSize - remainingRestoreSize
|
||||
|
||||
val progress: Float? = when (this.restoreState) {
|
||||
RestoreState.CALCULATING_MEDIA,
|
||||
RestoreState.CANCELING_MEDIA -> this.completedRestoredSize.percentageOf(this.totalRestoreSize)
|
||||
|
||||
RestoreState.RESTORING_MEDIA -> {
|
||||
when (this.restoreStatus) {
|
||||
RestoreStatus.NONE -> null
|
||||
RestoreStatus.FINISHED -> 1f
|
||||
else -> this.completedRestoredSize.percentageOf(this.totalRestoreSize)
|
||||
}
|
||||
}
|
||||
|
||||
RestoreState.NONE -> {
|
||||
if (this.restoreStatus == RestoreStatus.FINISHED) {
|
||||
1f
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
else -> null
|
||||
}
|
||||
|
||||
fun activelyRestoring(): Boolean {
|
||||
return restoreState.inProgress
|
||||
}
|
||||
|
||||
fun needRestoreMediaService(): Boolean {
|
||||
return (restoreState == RestoreState.CALCULATING_MEDIA || restoreState == RestoreState.RESTORING_MEDIA) &&
|
||||
totalRestoreSize > 0.bytes &&
|
||||
remainingRestoreSize != 0.bytes
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes the status of an in-progress media download session.
|
||||
*/
|
||||
enum class RestoreStatus {
|
||||
NONE,
|
||||
RESTORING,
|
||||
LOW_BATTERY,
|
||||
WAITING_FOR_INTERNET,
|
||||
WAITING_FOR_WIFI,
|
||||
NOT_ENOUGH_DISK_SPACE,
|
||||
FINISHED
|
||||
}
|
||||
}
|
||||
@@ -41,8 +41,10 @@ import org.signal.core.util.getForeignKeyViolations
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.logging.logW
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.signal.core.util.requireBoolean
|
||||
import org.signal.core.util.requireIntOrNull
|
||||
import org.signal.core.util.requireNonNullString
|
||||
import org.signal.core.util.requireString
|
||||
import org.signal.core.util.stream.NonClosingOutputStream
|
||||
import org.signal.core.util.urlEncode
|
||||
import org.signal.core.util.withinTransaction
|
||||
@@ -57,7 +59,6 @@ import org.thoughtcrime.securesms.attachments.Cdn
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
|
||||
import org.thoughtcrime.securesms.backup.ArchiveUploadProgress
|
||||
import org.thoughtcrime.securesms.backup.DeletionState
|
||||
import org.thoughtcrime.securesms.backup.RestoreState
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository.copyAttachmentToArchive
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository.exportForDebugging
|
||||
import org.thoughtcrime.securesms.backup.v2.importer.ChatItemArchiveImporter
|
||||
@@ -91,29 +92,31 @@ import org.thoughtcrime.securesms.database.OneTimePreKeyTable
|
||||
import org.thoughtcrime.securesms.database.SearchTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.SignedPreKeyTable
|
||||
import org.thoughtcrime.securesms.database.StickerTable
|
||||
import org.thoughtcrime.securesms.database.ThreadTable
|
||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.jobmanager.Job
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.DataRestoreConstraint
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.WifiConstraint
|
||||
import org.thoughtcrime.securesms.jobs.ArchiveAttachmentBackfillJob
|
||||
import org.thoughtcrime.securesms.jobs.AvatarGroupsV2DownloadJob
|
||||
import org.thoughtcrime.securesms.jobs.BackupDeleteJob
|
||||
import org.thoughtcrime.securesms.jobs.BackupMessagesJob
|
||||
import org.thoughtcrime.securesms.jobs.BackupRestoreMediaJob
|
||||
import org.thoughtcrime.securesms.jobs.CheckRestoreMediaLeftJob
|
||||
import org.thoughtcrime.securesms.jobs.CancelRestoreMediaJob
|
||||
import org.thoughtcrime.securesms.jobs.CreateReleaseChannelJob
|
||||
import org.thoughtcrime.securesms.jobs.LocalBackupJob
|
||||
import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob
|
||||
import org.thoughtcrime.securesms.jobs.RestoreAttachmentJob
|
||||
import org.thoughtcrime.securesms.jobs.RestoreOptimizedMediaJob
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob
|
||||
import org.thoughtcrime.securesms.jobs.StickerPackDownloadJob
|
||||
import org.thoughtcrime.securesms.jobs.UploadAttachmentToArchiveJob
|
||||
import org.thoughtcrime.securesms.keyvalue.BackupValues.ArchiveServiceCredentials
|
||||
import org.thoughtcrime.securesms.keyvalue.KeyValueStore
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.keyvalue.isDecisionPending
|
||||
import org.thoughtcrime.securesms.keyvalue.protos.ArchiveUploadProgressState
|
||||
import org.thoughtcrime.securesms.logsubmit.SubmitDebugLogRepository
|
||||
import org.thoughtcrime.securesms.net.SignalNetwork
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels
|
||||
@@ -121,6 +124,7 @@ import org.thoughtcrime.securesms.notifications.NotificationIds
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.service.BackupMediaRestoreService
|
||||
import org.thoughtcrime.securesms.service.BackupProgressService
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||
@@ -172,7 +176,7 @@ import kotlin.time.Duration.Companion.seconds
|
||||
object BackupRepository {
|
||||
|
||||
private val TAG = Log.tag(BackupRepository::class.java)
|
||||
private const val VERSION = 1L
|
||||
const val VERSION = 1L
|
||||
private const val REMOTE_MAIN_DB_SNAPSHOT_NAME = "remote-signal-snapshot"
|
||||
private const val REMOTE_KEYVALUE_DB_SNAPSHOT_NAME = "remote-signal-key-value-snapshot"
|
||||
private const val LOCAL_MAIN_DB_SNAPSHOT_NAME = "local-signal-snapshot"
|
||||
@@ -357,11 +361,7 @@ object BackupRepository {
|
||||
*/
|
||||
@JvmStatic
|
||||
fun skipMediaRestore() {
|
||||
SignalStore.backup.userManuallySkippedMediaRestore = true
|
||||
|
||||
RestoreAttachmentJob.Queues.ALL.forEach { AppDependencies.jobManager.cancelAllInQueue(it) }
|
||||
|
||||
RestoreAttachmentJob.Queues.ALL.forEach { AppDependencies.jobManager.add(CheckRestoreMediaLeftJob(it)) }
|
||||
CancelRestoreMediaJob.enqueue()
|
||||
}
|
||||
|
||||
fun markBackupFailure() {
|
||||
@@ -374,29 +374,6 @@ object BackupRepository {
|
||||
}
|
||||
}
|
||||
|
||||
fun displayManualBackupNotCreatedInThresholdNotification() {
|
||||
if (SignalStore.backup.lastBackupTime <= 0) {
|
||||
return
|
||||
}
|
||||
|
||||
val daysSinceLastBackup = (System.currentTimeMillis().milliseconds - SignalStore.backup.lastBackupTime.milliseconds).inWholeDays.toInt()
|
||||
val context = AppDependencies.application
|
||||
val pendingIntent = PendingIntent.getActivity(context, 0, AppSettingsActivity.remoteBackups(context), cancelCurrent())
|
||||
val notification = NotificationCompat.Builder(context, NotificationChannels.getInstance().APP_ALERTS)
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setContentTitle(context.resources.getQuantityString(R.plurals.Notification_no_backup_for_d_days, daysSinceLastBackup, daysSinceLastBackup))
|
||||
.setContentText(context.resources.getQuantityString(R.plurals.Notification_you_have_not_completed_a_backup, daysSinceLastBackup, daysSinceLastBackup))
|
||||
.setContentIntent(pendingIntent)
|
||||
.setAutoCancel(true)
|
||||
.build()
|
||||
|
||||
ServiceUtil.getNotificationManager(context).notify(NotificationIds.MANUAL_BACKUP_NOT_CREATED, notification)
|
||||
}
|
||||
|
||||
fun cancelManualBackupNotCreatedInThresholdNotification() {
|
||||
ServiceUtil.getNotificationManager(AppDependencies.application).cancel(NotificationIds.MANUAL_BACKUP_NOT_CREATED)
|
||||
}
|
||||
|
||||
@Discouraged("This is only public to allow internal settings to call it directly.")
|
||||
fun displayInitialBackupFailureNotification() {
|
||||
val context = AppDependencies.application
|
||||
@@ -520,45 +497,6 @@ object BackupRepository {
|
||||
SignalStore.backup.hasBackupAlreadyRedeemedError = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the "No backup" for manual backups should be displayed.
|
||||
* This should only be displayed after a set threshold has passed and the user
|
||||
* has set the MANUAL backups frequency.
|
||||
*/
|
||||
fun shouldDisplayNoManualBackupForTimeoutSheet(): Boolean {
|
||||
if (shouldNotDisplayBackupFailedMessaging()) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (SignalStore.backup.backupFrequency != BackupFrequency.MANUAL) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (SignalStore.backup.lastBackupTime <= 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
val isNetworkConstraintMet = if (SignalStore.backup.backupWithCellular) {
|
||||
NetworkConstraint.isMet(AppDependencies.application)
|
||||
} else {
|
||||
WifiConstraint.isMet(AppDependencies.application)
|
||||
}
|
||||
|
||||
if (!isNetworkConstraintMet) {
|
||||
return false
|
||||
}
|
||||
|
||||
val durationSinceLastBackup = System.currentTimeMillis().milliseconds - SignalStore.backup.lastBackupTime.milliseconds
|
||||
if (durationSinceLastBackup < MANUAL_BACKUP_NOTIFICATION_THRESHOLD) {
|
||||
return false
|
||||
}
|
||||
|
||||
val display = !SignalStore.backup.isNoBackupForManualUploadNotified
|
||||
SignalStore.backup.isNoBackupForManualUploadNotified = false
|
||||
|
||||
return display
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the watermark for the indicator display.
|
||||
*/
|
||||
@@ -610,6 +548,29 @@ object BackupRepository {
|
||||
SignalStore.backup.snoozeDownloadNotifier()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun maybeFixAnyDanglingUploadProgress() {
|
||||
if (SignalStore.backup.archiveUploadState?.backupPhase == ArchiveUploadProgressState.BackupPhase.Message && AppDependencies.jobManager.find { it.factoryKey == BackupMessagesJob.KEY }.isEmpty()) {
|
||||
SignalStore.backup.archiveUploadState = null
|
||||
BackupMessagesJob.enqueue()
|
||||
return
|
||||
}
|
||||
|
||||
if (!SignalStore.backup.backsUpMedia || !AppDependencies.jobManager.areQueuesEmpty(UploadAttachmentToArchiveJob.QUEUES)) {
|
||||
return
|
||||
}
|
||||
|
||||
val pendingBytes = SignalDatabase.attachments.getPendingArchiveUploadBytes()
|
||||
if (pendingBytes == 0L) {
|
||||
return
|
||||
}
|
||||
|
||||
Log.w(TAG, "There are ${pendingBytes.bytes.toUnitString(maxPlaces = 2)} of attachments that need to be uploaded to the archive, but no jobs for them! Attempting to fix.")
|
||||
val resetCount = SignalDatabase.attachments.clearArchiveTransferStateForInProgressItems()
|
||||
Log.w(TAG, "Cleared the archive transfer state of $resetCount attachments.")
|
||||
AppDependencies.jobManager.add(ArchiveAttachmentBackfillJob())
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the "Your media will be deleted today" sheet should be displayed.
|
||||
*/
|
||||
@@ -1373,6 +1334,17 @@ object BackupRepository {
|
||||
AppDependencies.recipientCache.warmUp()
|
||||
SignalDatabase.threads.clearCache()
|
||||
|
||||
val stickerJobs = SignalDatabase.stickers.getAllStickerPacks().use { cursor ->
|
||||
val reader = StickerTable.StickerPackRecordReader(cursor)
|
||||
reader
|
||||
.filter { it.isInstalled }
|
||||
.map {
|
||||
StickerPackDownloadJob.forInstall(it.packId, it.packKey, false)
|
||||
}
|
||||
}
|
||||
AppDependencies.jobManager.addAll(stickerJobs)
|
||||
stopwatch.split("sticker-jobs")
|
||||
|
||||
val recipientIds = SignalDatabase.threads.getRecentConversationList(
|
||||
limit = RECENT_RECIPIENTS_MAX,
|
||||
includeInactiveGroups = false,
|
||||
@@ -1390,6 +1362,7 @@ object BackupRepository {
|
||||
}
|
||||
|
||||
RetrieveProfileJob.enqueue(recipientIds, skipDebounce = false)
|
||||
stopwatch.split("profile-jobs")
|
||||
|
||||
AppDependencies.jobManager.add(CreateReleaseChannelJob.create())
|
||||
|
||||
@@ -1466,6 +1439,7 @@ object BackupRepository {
|
||||
|
||||
fun enablePaidBackupTier() {
|
||||
Log.i(TAG, "Setting backup tier to PAID", true)
|
||||
resetInitializedStateAndAuthCredentials()
|
||||
SignalStore.backup.backupTier = MessageBackupTier.PAID
|
||||
SignalStore.backup.lastCheckInMillis = System.currentTimeMillis()
|
||||
SignalStore.backup.lastCheckInSnoozeMillis = 0
|
||||
@@ -2018,7 +1992,7 @@ object BackupRepository {
|
||||
|
||||
suspend fun restoreRemoteBackup(): RemoteRestoreResult {
|
||||
val context = AppDependencies.application
|
||||
SignalStore.backup.restoreState = RestoreState.PENDING
|
||||
ArchiveRestoreProgress.onRestorePending()
|
||||
|
||||
try {
|
||||
DataRestoreConstraint.isRestoringData = true
|
||||
@@ -2033,7 +2007,7 @@ object BackupRepository {
|
||||
}
|
||||
|
||||
private fun restoreRemoteBackup(controller: BackupProgressService.Controller, cancellationSignal: () -> Boolean): RemoteRestoreResult {
|
||||
SignalStore.backup.restoreState = RestoreState.RESTORING_DB
|
||||
ArchiveRestoreProgress.onRestoringDb()
|
||||
|
||||
val progressListener = object : ProgressListener {
|
||||
override fun onAttachmentProgress(progress: AttachmentTransferProgress) {
|
||||
@@ -2125,8 +2099,7 @@ object BackupRepository {
|
||||
return RemoteRestoreResult.Failure
|
||||
}
|
||||
|
||||
SignalStore.backup.restoreState = RestoreState.RESTORING_MEDIA
|
||||
|
||||
BackupMediaRestoreService.resetTimeout()
|
||||
AppDependencies.jobManager.add(BackupRestoreMediaJob())
|
||||
|
||||
Log.i(TAG, "[remoteRestore] Restore successful")
|
||||
@@ -2135,7 +2108,7 @@ object BackupRepository {
|
||||
|
||||
suspend fun restoreLinkAndSyncBackup(response: TransferArchiveResponse, ephemeralBackupKey: MessageBackupKey) {
|
||||
val context = AppDependencies.application
|
||||
SignalStore.backup.restoreState = RestoreState.PENDING
|
||||
ArchiveRestoreProgress.onRestorePending()
|
||||
|
||||
try {
|
||||
DataRestoreConstraint.isRestoringData = true
|
||||
@@ -2150,7 +2123,7 @@ object BackupRepository {
|
||||
}
|
||||
|
||||
private fun restoreLinkAndSyncBackup(response: TransferArchiveResponse, ephemeralBackupKey: MessageBackupKey, controller: BackupProgressService.Controller, cancellationSignal: () -> Boolean): RemoteRestoreResult {
|
||||
SignalStore.backup.restoreState = RestoreState.RESTORING_DB
|
||||
ArchiveRestoreProgress.onRestoringDb()
|
||||
|
||||
val progressListener = object : ProgressListener {
|
||||
override fun onAttachmentProgress(progress: AttachmentTransferProgress) {
|
||||
@@ -2201,8 +2174,7 @@ object BackupRepository {
|
||||
return RemoteRestoreResult.Failure
|
||||
}
|
||||
|
||||
SignalStore.backup.restoreState = RestoreState.RESTORING_MEDIA
|
||||
|
||||
BackupMediaRestoreService.resetTimeout()
|
||||
AppDependencies.jobManager.add(BackupRestoreMediaJob())
|
||||
|
||||
Log.i(TAG, "[restoreLinkAndSyncBackup] Restore successful")
|
||||
@@ -2357,11 +2329,22 @@ class ArchiveMediaItemIterator(private val cursor: Cursor) : Iterator<ArchiveMed
|
||||
val plaintextHash = cursor.requireNonNullString(AttachmentTable.DATA_HASH_END).decodeBase64OrThrow()
|
||||
val remoteKey = cursor.requireNonNullString(AttachmentTable.REMOTE_KEY).decodeBase64OrThrow()
|
||||
val cdn = cursor.requireIntOrNull(AttachmentTable.ARCHIVE_CDN)
|
||||
val quote = cursor.requireBoolean(AttachmentTable.QUOTE)
|
||||
val contentType = cursor.requireString(AttachmentTable.CONTENT_TYPE)
|
||||
|
||||
val mediaId = MediaName.fromPlaintextHashAndRemoteKey(plaintextHash, remoteKey).toMediaId(SignalStore.backup.mediaRootBackupKey).encode()
|
||||
val thumbnailMediaId = MediaName.fromPlaintextHashAndRemoteKeyForThumbnail(plaintextHash, remoteKey).toMediaId(SignalStore.backup.mediaRootBackupKey).encode()
|
||||
|
||||
cursor.moveToNext()
|
||||
return ArchiveMediaItem(mediaId, thumbnailMediaId, cdn, plaintextHash, remoteKey)
|
||||
|
||||
return ArchiveMediaItem(
|
||||
mediaId = mediaId,
|
||||
thumbnailMediaId = thumbnailMediaId,
|
||||
cdn = cdn,
|
||||
plaintextHash = plaintextHash,
|
||||
remoteKey = remoteKey,
|
||||
quote = quote,
|
||||
contentType = contentType
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,6 +113,7 @@ import org.thoughtcrime.securesms.backup.v2.proto.GiftBadge as BackupGiftBadge
|
||||
private val TAG = Log.tag(ChatItemArchiveExporter::class.java)
|
||||
private val MAX_INLINED_BODY_SIZE = 128.kibiBytes.bytes.toInt()
|
||||
private val MAX_INLINED_BODY_SIZE_WITH_LONG_ATTACHMENT_POINTER = 2.kibiBytes.bytes.toInt()
|
||||
private val MAX_INLINED_QUOTE_BODY_SIZE = 2.kibiBytes.bytes.toInt()
|
||||
|
||||
/**
|
||||
* An iterator for chat items with a clever performance twist: rather than do the extra queries one at a time (for reactions,
|
||||
@@ -460,7 +461,7 @@ class ChatItemArchiveExporter(
|
||||
|
||||
val attachmentsFuture = executor.submitTyped {
|
||||
extraDataTimer.timeEvent("attachments") {
|
||||
db.attachmentTable.getAttachmentsForMessages(messageIds)
|
||||
db.attachmentTable.getAttachmentsForMessages(messageIds, excludeTranscodingQuotes = true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1071,7 +1072,7 @@ private fun BackupMessageRecord.toRemoteQuote(exportState: ExportState, attachme
|
||||
val localType = QuoteModel.Type.fromCode(this.quoteType)
|
||||
val remoteType = when (localType) {
|
||||
QuoteModel.Type.NORMAL -> {
|
||||
if (attachments?.any { it.contentType == MediaUtil.VIEW_ONCE } == true) {
|
||||
if (attachments?.any { it.quoteTargetContentType == MediaUtil.VIEW_ONCE } == true) {
|
||||
Quote.Type.VIEW_ONCE
|
||||
} else {
|
||||
Quote.Type.NORMAL
|
||||
@@ -1081,7 +1082,8 @@ private fun BackupMessageRecord.toRemoteQuote(exportState: ExportState, attachme
|
||||
}
|
||||
|
||||
val bodyRanges = this.quoteBodyRanges?.toRemoteBodyRanges(dateSent) ?: emptyList()
|
||||
val body = this.quoteBody?.takeUnless { it.isBlank() }?.let { body ->
|
||||
val trimmedQuoteBody = StringUtil.trimToFit(this.quoteBody, MAX_INLINED_QUOTE_BODY_SIZE)
|
||||
val body = trimmedQuoteBody.takeUnless { it.isBlank() }?.let { body ->
|
||||
Text(
|
||||
body = body,
|
||||
bodyRanges = bodyRanges
|
||||
@@ -1158,11 +1160,11 @@ private fun DatabaseAttachment.toRemoteStickerMessage(sentTimestamp: Long, react
|
||||
private fun List<DatabaseAttachment>.toRemoteQuoteAttachments(): List<Quote.QuotedAttachment> {
|
||||
return this.map { attachment ->
|
||||
Quote.QuotedAttachment(
|
||||
contentType = attachment.contentType,
|
||||
contentType = attachment.quoteTargetContentType,
|
||||
fileName = attachment.fileName,
|
||||
thumbnail = attachment.toRemoteMessageAttachment(
|
||||
flagOverride = MessageAttachment.Flag.NONE,
|
||||
contentTypeOverride = "image/jpeg"
|
||||
contentTypeOverride = attachment.contentType
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -76,7 +76,6 @@ import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.stickers.StickerLocator
|
||||
import org.thoughtcrime.securesms.util.JsonUtils
|
||||
import org.thoughtcrime.securesms.util.MediaUtil
|
||||
import org.thoughtcrime.securesms.util.MessageUtil
|
||||
import org.whispersystems.signalservice.api.payments.Money
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
@@ -1059,8 +1058,8 @@ class ChatItemArchiveImporter(
|
||||
else -> null
|
||||
}
|
||||
},
|
||||
start = bodyRange.start ?: 0,
|
||||
length = bodyRange.length ?: 0
|
||||
start = bodyRange.start,
|
||||
length = bodyRange.length
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -1090,11 +1089,11 @@ class ChatItemArchiveImporter(
|
||||
|
||||
private fun Quote.toLocalAttachments(): List<Attachment> {
|
||||
if (this.type == Quote.Type.VIEW_ONCE) {
|
||||
return listOf(TombstoneAttachment(contentType = MediaUtil.VIEW_ONCE, quote = true))
|
||||
return listOf(TombstoneAttachment.forQuote())
|
||||
}
|
||||
|
||||
return attachments.mapNotNull { attachment ->
|
||||
val thumbnail = attachment.thumbnail?.toLocalAttachment(quote = true)
|
||||
return this.attachments.mapNotNull { attachment ->
|
||||
val thumbnail = attachment.thumbnail?.toLocalAttachment(quote = true, quoteTargetContentType = attachment.contentType)
|
||||
|
||||
if (thumbnail != null) {
|
||||
return@mapNotNull thumbnail
|
||||
@@ -1141,7 +1140,7 @@ class ChatItemArchiveImporter(
|
||||
)
|
||||
}
|
||||
|
||||
private fun MessageAttachment.toLocalAttachment(quote: Boolean = false, contentType: String? = pointer?.contentType): Attachment? {
|
||||
private fun MessageAttachment.toLocalAttachment(quote: Boolean = false, quoteTargetContentType: String? = null, contentType: String? = pointer?.contentType): Attachment? {
|
||||
return pointer?.toLocalAttachment(
|
||||
voiceNote = flag == MessageAttachment.Flag.VOICE_MESSAGE,
|
||||
borderless = flag == MessageAttachment.Flag.BORDERLESS,
|
||||
@@ -1150,7 +1149,8 @@ class ChatItemArchiveImporter(
|
||||
contentType = contentType,
|
||||
fileName = pointer.fileName,
|
||||
uuid = clientUuid,
|
||||
quote = quote
|
||||
quote = quote,
|
||||
quoteTargetContentType = quoteTargetContentType
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -18,8 +18,6 @@ import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.StickerTable
|
||||
import org.thoughtcrime.securesms.database.StickerTable.StickerPackRecordReader
|
||||
import org.thoughtcrime.securesms.database.model.StickerPackRecord
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.jobs.StickerPackDownloadJob
|
||||
import java.io.IOException
|
||||
|
||||
private val TAG = Log.tag(StickerArchiveProcessor::class)
|
||||
@@ -55,14 +53,6 @@ object StickerArchiveProcessor {
|
||||
StickerTable.FILE_PATH to ""
|
||||
)
|
||||
.run(SQLiteDatabase.CONFLICT_IGNORE)
|
||||
|
||||
AppDependencies.jobManager.add(
|
||||
StickerPackDownloadJob.forInstall(
|
||||
Hex.toStringCondensed(stickerPack.packId.toByteArray()),
|
||||
Hex.toStringCondensed(stickerPack.packKey.toByteArray()),
|
||||
false
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,10 +38,6 @@ object BackupAlertDelegate {
|
||||
} else if (BackupRepository.shouldDisplayBackupExpiredAndDowngradedSheet()) {
|
||||
Log.d(TAG, "Displaying ExpiredAndDowngraded sheet.")
|
||||
BackupAlertBottomSheet.create(BackupAlert.ExpiredAndDowngraded).show(fragmentManager, FRAGMENT_TAG)
|
||||
} else if (BackupRepository.shouldDisplayNoManualBackupForTimeoutSheet()) {
|
||||
Log.d(TAG, "Displaying NoManualBackupBottomSheet.")
|
||||
NoManualBackupBottomSheet().show(fragmentManager, FRAGMENT_TAG)
|
||||
BackupRepository.displayManualBackupNotCreatedInThresholdNotification()
|
||||
} else if (BackupRepository.shouldDisplayOutOfRemoteStorageSpaceSheet()) {
|
||||
Log.d(TAG, "Displaying NoRemoteStorageSpaceAvailableBottomSheet.")
|
||||
NoRemoteStorageSpaceAvailableBottomSheet().show(fragmentManager, FRAGMENT_TAG)
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2.ui.status
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.text.InlineTextContent
|
||||
import androidx.compose.foundation.text.appendInlineContent
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.LinkAnnotation
|
||||
import androidx.compose.ui.text.Placeholder
|
||||
import androidx.compose.ui.text.PlaceholderVerticalAlign
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.TextLinkStyles
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.withLink
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.SignalPreview
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.signal.core.ui.R as CoreUiR
|
||||
|
||||
private val YELLOW_DOT = Color(0xFFFFCC00)
|
||||
|
||||
/**
|
||||
* Show backup creation failures as a settings row.
|
||||
*/
|
||||
@Composable
|
||||
fun BackupCreateErrorRow(
|
||||
showCouldNotComplete: Boolean,
|
||||
showBackupFailed: Boolean,
|
||||
onLearnMoreClick: () -> Unit = {}
|
||||
) {
|
||||
if (showBackupFailed) {
|
||||
val inlineContentMap = mapOf(
|
||||
"yellow_bullet" to InlineTextContent(
|
||||
Placeholder(20.sp, 12.sp, PlaceholderVerticalAlign.TextCenter)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(12.dp)
|
||||
.background(color = YELLOW_DOT, shape = CircleShape)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
BackupAlertText(
|
||||
text = buildAnnotatedString {
|
||||
appendInlineContent("yellow_bullet")
|
||||
append(" ")
|
||||
append(stringResource(R.string.BackupStatusRow__your_last_backup_latest_version))
|
||||
append(" ")
|
||||
withLink(
|
||||
LinkAnnotation.Clickable(
|
||||
stringResource(R.string.BackupStatusRow__learn_more),
|
||||
styles = TextLinkStyles(style = SpanStyle(color = MaterialTheme.colorScheme.primary))
|
||||
) {
|
||||
onLearnMoreClick()
|
||||
}
|
||||
) {
|
||||
append(stringResource(R.string.BackupStatusRow__learn_more))
|
||||
}
|
||||
},
|
||||
inlineContent = inlineContentMap
|
||||
)
|
||||
} else if (showCouldNotComplete) {
|
||||
val inlineContentMap = mapOf(
|
||||
"yellow_bullet" to InlineTextContent(
|
||||
Placeholder(20.sp, 12.sp, PlaceholderVerticalAlign.TextCenter)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(12.dp)
|
||||
.background(color = YELLOW_DOT, shape = CircleShape)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
BackupAlertText(
|
||||
text = buildAnnotatedString {
|
||||
appendInlineContent("yellow_bullet")
|
||||
append(" ")
|
||||
append(stringResource(R.string.BackupStatusRow__your_last_backup))
|
||||
},
|
||||
inlineContent = inlineContentMap
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BackupAlertText(text: AnnotatedString, inlineContent: Map<String, InlineTextContent>) {
|
||||
Text(
|
||||
text = text,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(horizontal = dimensionResource(CoreUiR.dimen.gutter)),
|
||||
inlineContent = inlineContent
|
||||
)
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
fun BackupStatusRowCouldNotCompleteBackupPreview() {
|
||||
Previews.Preview {
|
||||
BackupCreateErrorRow(showCouldNotComplete = true, showBackupFailed = false)
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
fun BackupStatusRowBackupFailedPreview() {
|
||||
Previews.Preview {
|
||||
BackupCreateErrorRow(showCouldNotComplete = false, showBackupFailed = true)
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,6 @@
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2.ui.status
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
@@ -31,6 +29,7 @@ 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.Color
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -38,14 +37,12 @@ import androidx.compose.ui.unit.dp
|
||||
import org.signal.core.ui.compose.Buttons
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.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.RestoreState
|
||||
import org.thoughtcrime.securesms.backup.v2.ArchiveRestoreProgressState
|
||||
import org.thoughtcrime.securesms.backup.v2.ArchiveRestoreProgressState.RestoreStatus
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.BackupsIconColors
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
private const val NONE = -1
|
||||
|
||||
@@ -56,12 +53,16 @@ private const val NONE = -1
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun BackupStatusBanner(
|
||||
data: BackupStatusData,
|
||||
data: ArchiveRestoreProgressState,
|
||||
onBannerClick: () -> Unit = {},
|
||||
onActionClick: (BackupStatusData) -> Unit = {},
|
||||
onActionClick: (ArchiveRestoreProgressState) -> Unit = {},
|
||||
onDismissClick: () -> Unit = {},
|
||||
contentPadding: PaddingValues = PaddingValues(horizontal = 12.dp, vertical = 8.dp)
|
||||
) {
|
||||
if (!data.restoreState.isMediaRestoreOperation && data.restoreStatus != RestoreStatus.FINISHED) {
|
||||
return
|
||||
}
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
@@ -73,9 +74,9 @@ fun BackupStatusBanner(
|
||||
.padding(12.dp)
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = data.iconRes),
|
||||
painter = painterResource(id = data.iconResource()),
|
||||
contentDescription = null,
|
||||
tint = data.iconColors.foreground,
|
||||
tint = data.iconColor(),
|
||||
modifier = Modifier
|
||||
.padding(start = 4.dp)
|
||||
.size(24.dp)
|
||||
@@ -89,7 +90,7 @@ fun BackupStatusBanner(
|
||||
.weight(1f)
|
||||
) {
|
||||
Text(
|
||||
text = data.title,
|
||||
text = data.title(),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier
|
||||
@@ -97,7 +98,7 @@ fun BackupStatusBanner(
|
||||
.align(Alignment.CenterVertically)
|
||||
)
|
||||
|
||||
data.status?.let { status ->
|
||||
data.status()?.let { status ->
|
||||
Text(
|
||||
text = status,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
@@ -109,9 +110,12 @@ fun BackupStatusBanner(
|
||||
}
|
||||
}
|
||||
|
||||
if (data.progress >= 0f) {
|
||||
if (data.restoreState == RestoreState.CALCULATING_MEDIA ||
|
||||
data.restoreState == RestoreState.CANCELING_MEDIA ||
|
||||
(data.restoreState == RestoreState.RESTORING_MEDIA && data.restoreStatus == RestoreStatus.RESTORING)
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
progress = { data.progress },
|
||||
progress = { data.progress!! },
|
||||
strokeWidth = 3.dp,
|
||||
strokeCap = StrokeCap.Round,
|
||||
modifier = Modifier
|
||||
@@ -119,16 +123,16 @@ fun BackupStatusBanner(
|
||||
)
|
||||
}
|
||||
|
||||
if (data.actionRes != NONE) {
|
||||
if (data.actionResource() != NONE) {
|
||||
Buttons.Small(
|
||||
onClick = { onActionClick(data) },
|
||||
modifier = Modifier.padding(start = 8.dp)
|
||||
) {
|
||||
Text(text = stringResource(id = data.actionRes))
|
||||
Text(text = stringResource(id = data.actionResource()))
|
||||
}
|
||||
}
|
||||
|
||||
if (data.showDismissAction) {
|
||||
if (data.restoreStatus == RestoreStatus.FINISHED) {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
|
||||
Icon(
|
||||
@@ -147,195 +151,208 @@ fun BackupStatusBanner(
|
||||
}
|
||||
}
|
||||
|
||||
private fun ArchiveRestoreProgressState.iconResource(): Int {
|
||||
return when (this.restoreState) {
|
||||
RestoreState.CALCULATING_MEDIA,
|
||||
RestoreState.CANCELING_MEDIA -> R.drawable.symbol_backup_light
|
||||
|
||||
RestoreState.RESTORING_MEDIA -> {
|
||||
when (this.restoreStatus) {
|
||||
RestoreStatus.RESTORING,
|
||||
RestoreStatus.WAITING_FOR_INTERNET,
|
||||
RestoreStatus.WAITING_FOR_WIFI,
|
||||
RestoreStatus.LOW_BATTERY -> R.drawable.symbol_backup_light
|
||||
|
||||
RestoreStatus.NOT_ENOUGH_DISK_SPACE -> R.drawable.symbol_backup_error_24
|
||||
RestoreStatus.FINISHED -> R.drawable.symbol_check_circle_24
|
||||
RestoreStatus.NONE -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
|
||||
RestoreState.NONE -> {
|
||||
if (this.restoreStatus == RestoreStatus.FINISHED) {
|
||||
R.drawable.symbol_check_circle_24
|
||||
} else {
|
||||
throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
|
||||
RestoreState.PENDING,
|
||||
RestoreState.RESTORING_DB -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ArchiveRestoreProgressState.iconColor(): Color {
|
||||
return when (this.restoreState) {
|
||||
RestoreState.CALCULATING_MEDIA,
|
||||
RestoreState.CANCELING_MEDIA -> BackupsIconColors.Normal.foreground
|
||||
|
||||
RestoreState.RESTORING_MEDIA -> {
|
||||
when (this.restoreStatus) {
|
||||
RestoreStatus.RESTORING -> BackupsIconColors.Normal.foreground
|
||||
RestoreStatus.WAITING_FOR_INTERNET,
|
||||
RestoreStatus.WAITING_FOR_WIFI,
|
||||
RestoreStatus.LOW_BATTERY,
|
||||
RestoreStatus.NOT_ENOUGH_DISK_SPACE -> BackupsIconColors.Warning.foreground
|
||||
|
||||
RestoreStatus.FINISHED -> BackupsIconColors.Success.foreground
|
||||
RestoreStatus.NONE -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
|
||||
RestoreState.NONE -> {
|
||||
if (this.restoreStatus == RestoreStatus.FINISHED) {
|
||||
BackupsIconColors.Success.foreground
|
||||
} else {
|
||||
throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
|
||||
RestoreState.PENDING,
|
||||
RestoreState.RESTORING_DB -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ArchiveRestoreProgressState.title(): String {
|
||||
return when (this.restoreState) {
|
||||
RestoreState.CALCULATING_MEDIA -> stringResource(R.string.BackupStatus__restoring_media)
|
||||
RestoreState.CANCELING_MEDIA -> stringResource(R.string.BackupStatus__cancel_restore_media)
|
||||
RestoreState.RESTORING_MEDIA -> {
|
||||
when (this.restoreStatus) {
|
||||
RestoreStatus.RESTORING -> stringResource(R.string.BackupStatus__restoring_media)
|
||||
RestoreStatus.WAITING_FOR_INTERNET,
|
||||
RestoreStatus.WAITING_FOR_WIFI,
|
||||
RestoreStatus.LOW_BATTERY -> stringResource(R.string.BackupStatus__restore_paused)
|
||||
|
||||
RestoreStatus.NOT_ENOUGH_DISK_SPACE -> {
|
||||
stringResource(R.string.BackupStatus__free_up_s_of_space_to_download_your_media, this.remainingRestoreSize.toUnitString())
|
||||
}
|
||||
|
||||
RestoreStatus.FINISHED -> stringResource(R.string.BackupStatus__restore_complete)
|
||||
RestoreStatus.NONE -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
|
||||
RestoreState.NONE -> {
|
||||
if (this.restoreStatus == RestoreStatus.FINISHED) {
|
||||
stringResource(R.string.BackupStatus__restore_complete)
|
||||
} else {
|
||||
throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
|
||||
RestoreState.PENDING,
|
||||
RestoreState.RESTORING_DB -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ArchiveRestoreProgressState.status(): String? {
|
||||
return when (this.restoreState) {
|
||||
RestoreState.CALCULATING_MEDIA -> {
|
||||
stringResource(
|
||||
R.string.BackupStatus__status_size_of_size,
|
||||
this.completedRestoredSize.toUnitString(),
|
||||
this.totalRestoreSize.toUnitString()
|
||||
)
|
||||
}
|
||||
|
||||
RestoreState.CANCELING_MEDIA -> null
|
||||
RestoreState.RESTORING_MEDIA -> {
|
||||
when (this.restoreStatus) {
|
||||
RestoreStatus.RESTORING -> {
|
||||
stringResource(
|
||||
R.string.BackupStatus__status_size_of_size,
|
||||
this.completedRestoredSize.toUnitString(),
|
||||
this.totalRestoreSize.toUnitString()
|
||||
)
|
||||
}
|
||||
|
||||
RestoreStatus.WAITING_FOR_INTERNET -> stringResource(R.string.BackupStatus__status_no_internet)
|
||||
RestoreStatus.WAITING_FOR_WIFI -> stringResource(R.string.BackupStatus__status_waiting_for_wifi)
|
||||
RestoreStatus.LOW_BATTERY -> stringResource(R.string.BackupStatus__status_device_has_low_battery)
|
||||
RestoreStatus.NOT_ENOUGH_DISK_SPACE -> null
|
||||
RestoreStatus.FINISHED -> this.totalToRestoreThisRun.toUnitString()
|
||||
RestoreStatus.NONE -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
|
||||
RestoreState.NONE -> {
|
||||
if (this.restoreStatus == RestoreStatus.FINISHED) {
|
||||
this.totalToRestoreThisRun.toUnitString()
|
||||
} else {
|
||||
throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
|
||||
RestoreState.PENDING,
|
||||
RestoreState.RESTORING_DB -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
|
||||
private fun ArchiveRestoreProgressState.actionResource(): Int {
|
||||
return when (this.restoreState) {
|
||||
RestoreState.CALCULATING_MEDIA,
|
||||
RestoreState.CANCELING_MEDIA -> NONE
|
||||
|
||||
RestoreState.RESTORING_MEDIA -> {
|
||||
when (this.restoreStatus) {
|
||||
RestoreStatus.NOT_ENOUGH_DISK_SPACE -> R.string.BackupStatus__details
|
||||
RestoreStatus.WAITING_FOR_WIFI -> R.string.BackupStatus__resume
|
||||
else -> NONE
|
||||
}
|
||||
}
|
||||
|
||||
else -> NONE
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
fun BackupStatusBannerPreview() {
|
||||
Previews.Preview {
|
||||
Column {
|
||||
BackupStatusBanner(
|
||||
data = BackupStatusData.RestoringMedia(5755000.bytes, 1253.mebiBytes)
|
||||
data = ArchiveRestoreProgressState(restoreState = RestoreState.RESTORING_MEDIA, restoreStatus = RestoreStatus.RESTORING, remainingRestoreSize = 800.mebiBytes, totalRestoreSize = 1024.mebiBytes)
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
BackupStatusBanner(
|
||||
data = BackupStatusData.RestoringMedia(
|
||||
bytesDownloaded = 55000.bytes,
|
||||
bytesTotal = 1253.mebiBytes,
|
||||
restoreStatus = BackupStatusData.RestoreStatus.WAITING_FOR_WIFI
|
||||
)
|
||||
data = ArchiveRestoreProgressState(restoreState = RestoreState.CALCULATING_MEDIA, restoreStatus = RestoreStatus.RESTORING, remainingRestoreSize = 1024.mebiBytes, totalRestoreSize = 1024.mebiBytes)
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
BackupStatusBanner(
|
||||
data = BackupStatusData.RestoringMedia(
|
||||
bytesDownloaded = 55000.bytes,
|
||||
bytesTotal = 1253.mebiBytes,
|
||||
restoreStatus = BackupStatusData.RestoreStatus.WAITING_FOR_INTERNET
|
||||
)
|
||||
data = ArchiveRestoreProgressState(restoreState = RestoreState.CANCELING_MEDIA, restoreStatus = RestoreStatus.RESTORING, remainingRestoreSize = 200.mebiBytes, totalRestoreSize = 1024.mebiBytes)
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
BackupStatusBanner(
|
||||
data = BackupStatusData.RestoringMedia(
|
||||
bytesDownloaded = 55000.bytes,
|
||||
bytesTotal = 1253.mebiBytes,
|
||||
restoreStatus = BackupStatusData.RestoreStatus.FINISHED
|
||||
)
|
||||
data = ArchiveRestoreProgressState(restoreState = RestoreState.RESTORING_MEDIA, restoreStatus = RestoreStatus.WAITING_FOR_WIFI, remainingRestoreSize = 800.mebiBytes, totalRestoreSize = 1024.mebiBytes)
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
BackupStatusBanner(
|
||||
data = BackupStatusData.NotEnoughFreeSpace(40900.kibiBytes)
|
||||
data = ArchiveRestoreProgressState(restoreState = RestoreState.RESTORING_MEDIA, restoreStatus = RestoreStatus.WAITING_FOR_INTERNET, remainingRestoreSize = 800.mebiBytes, totalRestoreSize = 1024.mebiBytes)
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
BackupStatusBanner(
|
||||
data = BackupStatusData.CouldNotCompleteBackup
|
||||
data = ArchiveRestoreProgressState(restoreState = RestoreState.NONE, restoreStatus = RestoreStatus.FINISHED, remainingRestoreSize = 0.mebiBytes, totalRestoreSize = 0.mebiBytes, totalToRestoreThisRun = 1024.mebiBytes)
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
BackupStatusBanner(
|
||||
data = BackupStatusData.BackupFailed
|
||||
data = ArchiveRestoreProgressState(restoreState = RestoreState.RESTORING_MEDIA, restoreStatus = RestoreStatus.NOT_ENOUGH_DISK_SPACE, remainingRestoreSize = 500.mebiBytes, totalRestoreSize = 1024.mebiBytes)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sealed interface describing status data to display in BackupStatus widget.
|
||||
*/
|
||||
sealed interface BackupStatusData {
|
||||
|
||||
@get:DrawableRes
|
||||
val iconRes: Int
|
||||
|
||||
@get:Composable
|
||||
val title: String
|
||||
|
||||
val iconColors: BackupsIconColors
|
||||
|
||||
@get:StringRes
|
||||
val actionRes: 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_error_24
|
||||
|
||||
override val title: String
|
||||
@Composable
|
||||
get() = stringResource(androidx.biometric.R.string.default_error_msg)
|
||||
|
||||
override val iconColors: BackupsIconColors = BackupsIconColors.Warning
|
||||
}
|
||||
|
||||
/**
|
||||
* Initial backup creation failure
|
||||
*/
|
||||
data object BackupFailed : BackupStatusData {
|
||||
override val iconRes: Int = R.drawable.symbol_backup_error_24
|
||||
|
||||
override val title: String
|
||||
@Composable
|
||||
get() = stringResource(androidx.biometric.R.string.default_error_msg)
|
||||
|
||||
override val iconColors: BackupsIconColors = BackupsIconColors.Warning
|
||||
}
|
||||
|
||||
/**
|
||||
* User does not have enough space on their device to complete backup restoration
|
||||
*/
|
||||
class NotEnoughFreeSpace(
|
||||
requiredSpace: ByteSize
|
||||
) : BackupStatusData {
|
||||
val requiredSpace = requiredSpace.toUnitString()
|
||||
|
||||
override val iconRes: Int = R.drawable.symbol_backup_error_24
|
||||
|
||||
override val title: String
|
||||
@Composable
|
||||
get() = stringResource(R.string.BackupStatus__free_up_s_of_space_to_download_your_media, requiredSpace)
|
||||
|
||||
override val iconColors: BackupsIconColors = BackupsIconColors.Warning
|
||||
override val actionRes: Int = R.string.BackupStatus__details
|
||||
}
|
||||
|
||||
/**
|
||||
* Restoring media, finished, and paused states.
|
||||
*/
|
||||
data class RestoringMedia(
|
||||
val bytesDownloaded: ByteSize = 0.bytes,
|
||||
val bytesTotal: ByteSize = 0.bytes,
|
||||
val restoreStatus: RestoreStatus = RestoreStatus.NORMAL
|
||||
) : BackupStatusData {
|
||||
override val iconRes: Int = if (restoreStatus == RestoreStatus.FINISHED) R.drawable.symbol_check_circle_24 else R.drawable.symbol_backup_light
|
||||
override val iconColors: BackupsIconColors = when (restoreStatus) {
|
||||
RestoreStatus.FINISHED -> BackupsIconColors.Success
|
||||
RestoreStatus.NORMAL -> BackupsIconColors.Normal
|
||||
RestoreStatus.LOW_BATTERY,
|
||||
RestoreStatus.WAITING_FOR_INTERNET,
|
||||
RestoreStatus.WAITING_FOR_WIFI -> BackupsIconColors.Warning
|
||||
}
|
||||
override val showDismissAction: Boolean = restoreStatus == RestoreStatus.FINISHED
|
||||
override val actionRes: Int = when (restoreStatus) {
|
||||
RestoreStatus.WAITING_FOR_WIFI -> R.string.BackupStatus__resume
|
||||
else -> NONE
|
||||
}
|
||||
|
||||
override val title: String
|
||||
@Composable get() = stringResource(
|
||||
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 status: String
|
||||
@Composable get() = when (restoreStatus) {
|
||||
RestoreStatus.NORMAL -> stringResource(
|
||||
R.string.BackupStatus__status_size_of_size,
|
||||
bytesDownloaded.toUnitString(),
|
||||
bytesTotal.toUnitString()
|
||||
)
|
||||
|
||||
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 {
|
||||
NONE.toFloat()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes the status of an in-progress media download session.
|
||||
*/
|
||||
enum class RestoreStatus {
|
||||
NORMAL,
|
||||
LOW_BATTERY,
|
||||
WAITING_FOR_INTERNET,
|
||||
WAITING_FOR_WIFI,
|
||||
FINISHED
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,48 +5,34 @@
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2.ui.status
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.text.InlineTextContent
|
||||
import androidx.compose.foundation.text.appendInlineContent
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.LinkAnnotation
|
||||
import androidx.compose.ui.text.Placeholder
|
||||
import androidx.compose.ui.text.PlaceholderVerticalAlign
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.TextLinkStyles
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.withLink
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.Rows
|
||||
import org.signal.core.ui.compose.SignalPreview
|
||||
import org.signal.core.util.ByteSize
|
||||
import org.signal.core.util.mebiBytes
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.backup.RestoreState
|
||||
import org.thoughtcrime.securesms.backup.v2.ArchiveRestoreProgressState
|
||||
import org.thoughtcrime.securesms.backup.v2.ArchiveRestoreProgressState.RestoreStatus
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.BackupsIconColors
|
||||
import kotlin.math.roundToInt
|
||||
import org.signal.core.ui.R as CoreUiR
|
||||
|
||||
private val YELLOW_DOT = Color(0xFFFFCC00)
|
||||
|
||||
/**
|
||||
* Specifies what kind of restore this is. Slightly different messaging
|
||||
* is utilized for downloads.
|
||||
@@ -69,11 +55,10 @@ enum class RestoreType {
|
||||
*/
|
||||
@Composable
|
||||
fun BackupStatusRow(
|
||||
backupStatusData: BackupStatusData,
|
||||
backupStatusData: ArchiveRestoreProgressState,
|
||||
restoreType: RestoreType = RestoreType.RESTORE,
|
||||
onSkipClick: () -> Unit = {},
|
||||
onCancelClick: (() -> Unit)? = null,
|
||||
onLearnMoreClick: () -> Unit = {}
|
||||
onCancelClick: (() -> Unit)? = null
|
||||
) {
|
||||
val endPad = if (onCancelClick == null) {
|
||||
dimensionResource(CoreUiR.dimen.gutter)
|
||||
@@ -84,183 +69,155 @@ fun BackupStatusRow(
|
||||
Column(
|
||||
modifier = Modifier.padding(top = 8.dp, bottom = 12.dp)
|
||||
) {
|
||||
if (backupStatusData !is BackupStatusData.CouldNotCompleteBackup &&
|
||||
backupStatusData !is BackupStatusData.BackupFailed
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(
|
||||
start = dimensionResource(CoreUiR.dimen.gutter),
|
||||
end = endPad
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(
|
||||
start = dimensionResource(CoreUiR.dimen.gutter),
|
||||
end = endPad
|
||||
)
|
||||
) {
|
||||
LinearProgressIndicator(
|
||||
color = progressColor(backupStatusData),
|
||||
progress = { backupStatusData.progress },
|
||||
modifier = Modifier.weight(1f).padding(vertical = 12.dp),
|
||||
gapSize = 0.dp,
|
||||
drawStopIndicator = {}
|
||||
)
|
||||
LinearProgressIndicator(
|
||||
color = progressColor(backupStatusData),
|
||||
progress = { backupStatusData.progress ?: 0f },
|
||||
modifier = Modifier.weight(1f).padding(vertical = 12.dp),
|
||||
gapSize = 0.dp,
|
||||
drawStopIndicator = {}
|
||||
)
|
||||
|
||||
val isFinished = backupStatusData is BackupStatusData.RestoringMedia && backupStatusData.restoreStatus == BackupStatusData.RestoreStatus.FINISHED
|
||||
if (onCancelClick != null && !isFinished) {
|
||||
IconButton(
|
||||
onClick = onCancelClick
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.symbol_x_24),
|
||||
contentDescription = stringResource(R.string.BackupStatusRow__cancel_download)
|
||||
)
|
||||
}
|
||||
if (onCancelClick != null) {
|
||||
IconButton(
|
||||
onClick = onCancelClick
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.symbol_x_24),
|
||||
contentDescription = stringResource(R.string.BackupStatusRow__cancel_download)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
when (backupStatusData) {
|
||||
is BackupStatusData.RestoringMedia -> {
|
||||
val string = when (restoreType) {
|
||||
RestoreType.RESTORE -> getRestoringMediaString(backupStatusData)
|
||||
RestoreType.DOWNLOAD -> getDownloadingMediaString(backupStatusData)
|
||||
}
|
||||
if (backupStatusData.restoreStatus == RestoreStatus.NOT_ENOUGH_DISK_SPACE) {
|
||||
BackupAlertText(
|
||||
text = stringResource(R.string.BackupStatusRow__not_enough_space, backupStatusData.remainingRestoreSize)
|
||||
)
|
||||
|
||||
BackupAlertText(text = string)
|
||||
Rows.TextRow(
|
||||
text = stringResource(R.string.BackupStatusRow__skip_download),
|
||||
onClick = onSkipClick
|
||||
)
|
||||
} else {
|
||||
val string = when (restoreType) {
|
||||
RestoreType.RESTORE -> getRestoringMediaString(backupStatusData)
|
||||
RestoreType.DOWNLOAD -> getDownloadingMediaString(backupStatusData)
|
||||
}
|
||||
|
||||
is BackupStatusData.NotEnoughFreeSpace -> {
|
||||
BackupAlertText(
|
||||
text = stringResource(
|
||||
R.string.BackupStatusRow__not_enough_space,
|
||||
backupStatusData.requiredSpace,
|
||||
"%d".format((backupStatusData.progress * 100).roundToInt())
|
||||
)
|
||||
)
|
||||
|
||||
Rows.TextRow(
|
||||
text = stringResource(R.string.BackupStatusRow__skip_download),
|
||||
onClick = onSkipClick
|
||||
)
|
||||
}
|
||||
|
||||
BackupStatusData.CouldNotCompleteBackup -> {
|
||||
val inlineContentMap = mapOf(
|
||||
"yellow_bullet" to InlineTextContent(
|
||||
Placeholder(20.sp, 12.sp, PlaceholderVerticalAlign.TextCenter)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(12.dp)
|
||||
.background(color = YELLOW_DOT, shape = CircleShape)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
BackupAlertText(
|
||||
text = buildAnnotatedString {
|
||||
appendInlineContent("yellow_bullet")
|
||||
append(" ")
|
||||
append(stringResource(R.string.BackupStatusRow__your_last_backup))
|
||||
},
|
||||
inlineContent = inlineContentMap
|
||||
)
|
||||
}
|
||||
BackupStatusData.BackupFailed -> {
|
||||
val inlineContentMap = mapOf(
|
||||
"yellow_bullet" to InlineTextContent(
|
||||
Placeholder(20.sp, 12.sp, PlaceholderVerticalAlign.TextCenter)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(12.dp)
|
||||
.background(color = YELLOW_DOT, shape = CircleShape)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
BackupAlertText(
|
||||
text = buildAnnotatedString {
|
||||
appendInlineContent("yellow_bullet")
|
||||
append(" ")
|
||||
append(stringResource(R.string.BackupStatusRow__your_last_backup_latest_version))
|
||||
append(" ")
|
||||
withLink(
|
||||
LinkAnnotation.Clickable(
|
||||
stringResource(R.string.BackupStatusRow__learn_more),
|
||||
styles = TextLinkStyles(style = SpanStyle(color = MaterialTheme.colorScheme.primary))
|
||||
) {
|
||||
onLearnMoreClick()
|
||||
}
|
||||
) {
|
||||
append(stringResource(R.string.BackupStatusRow__learn_more))
|
||||
}
|
||||
},
|
||||
inlineContent = inlineContentMap
|
||||
)
|
||||
}
|
||||
BackupAlertText(text = string)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BackupAlertText(text: String) {
|
||||
BackupAlertText(
|
||||
text = remember(text) { AnnotatedString(text) },
|
||||
inlineContent = emptyMap()
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BackupAlertText(text: AnnotatedString, inlineContent: Map<String, InlineTextContent>) {
|
||||
Text(
|
||||
text = text,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(horizontal = dimensionResource(CoreUiR.dimen.gutter)),
|
||||
inlineContent = inlineContent
|
||||
modifier = Modifier.padding(horizontal = dimensionResource(CoreUiR.dimen.gutter))
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getRestoringMediaString(backupStatusData: BackupStatusData.RestoringMedia): String {
|
||||
return when (backupStatusData.restoreStatus) {
|
||||
BackupStatusData.RestoreStatus.NORMAL -> {
|
||||
private fun getRestoringMediaString(backupStatusData: ArchiveRestoreProgressState): String {
|
||||
return when (backupStatusData.restoreState) {
|
||||
RestoreState.CALCULATING_MEDIA -> {
|
||||
stringResource(
|
||||
R.string.BackupStatusRow__restoring_s_of_s_s,
|
||||
backupStatusData.bytesDownloaded.toUnitString(2),
|
||||
backupStatusData.bytesTotal.toUnitString(2),
|
||||
"%d".format((backupStatusData.progress * 100).roundToInt())
|
||||
backupStatusData.completedRestoredSize.toUnitString(2),
|
||||
backupStatusData.totalRestoreSize.toUnitString(2),
|
||||
"%d".format(((backupStatusData.progress ?: 0f) * 100).roundToInt())
|
||||
)
|
||||
}
|
||||
BackupStatusData.RestoreStatus.LOW_BATTERY -> stringResource(R.string.BackupStatusRow__restore_device_has_low_battery)
|
||||
BackupStatusData.RestoreStatus.WAITING_FOR_INTERNET -> stringResource(R.string.BackupStatusRow__restore_no_internet)
|
||||
BackupStatusData.RestoreStatus.WAITING_FOR_WIFI -> stringResource(R.string.BackupStatusRow__restore_waiting_for_wifi)
|
||||
BackupStatusData.RestoreStatus.FINISHED -> stringResource(R.string.BackupStatus__restore_complete)
|
||||
RestoreState.CANCELING_MEDIA -> stringResource(R.string.BackupStatus__cancel_restore_media)
|
||||
RestoreState.RESTORING_MEDIA -> {
|
||||
when (backupStatusData.restoreStatus) {
|
||||
RestoreStatus.RESTORING -> {
|
||||
stringResource(
|
||||
R.string.BackupStatusRow__restoring_s_of_s_s,
|
||||
backupStatusData.completedRestoredSize.toUnitString(2),
|
||||
backupStatusData.totalRestoreSize.toUnitString(2),
|
||||
"%d".format(((backupStatusData.progress ?: 0f) * 100).roundToInt())
|
||||
)
|
||||
}
|
||||
RestoreStatus.WAITING_FOR_INTERNET -> stringResource(R.string.BackupStatusRow__restore_no_internet)
|
||||
RestoreStatus.WAITING_FOR_WIFI -> stringResource(R.string.BackupStatusRow__restore_waiting_for_wifi)
|
||||
RestoreStatus.LOW_BATTERY -> stringResource(R.string.BackupStatusRow__restore_device_has_low_battery)
|
||||
RestoreStatus.FINISHED -> stringResource(R.string.BackupStatus__restore_complete)
|
||||
else -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
RestoreState.NONE -> {
|
||||
if (backupStatusData.restoreStatus == RestoreStatus.FINISHED) {
|
||||
stringResource(R.string.BackupStatus__restore_complete)
|
||||
} else {
|
||||
throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
RestoreState.PENDING,
|
||||
RestoreState.RESTORING_DB -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getDownloadingMediaString(backupStatusData: BackupStatusData.RestoringMedia): String {
|
||||
return when (backupStatusData.restoreStatus) {
|
||||
BackupStatusData.RestoreStatus.NORMAL -> {
|
||||
private fun getDownloadingMediaString(backupStatusData: ArchiveRestoreProgressState): String {
|
||||
return when (backupStatusData.restoreState) {
|
||||
RestoreState.CALCULATING_MEDIA -> {
|
||||
stringResource(
|
||||
R.string.BackupStatusRow__downloading_s_of_s_s,
|
||||
backupStatusData.bytesDownloaded.toUnitString(2),
|
||||
backupStatusData.bytesTotal.toUnitString(2),
|
||||
"%d".format((backupStatusData.progress * 100).roundToInt())
|
||||
backupStatusData.completedRestoredSize.toUnitString(2),
|
||||
backupStatusData.totalRestoreSize.toUnitString(2),
|
||||
"%d".format(((backupStatusData.progress ?: 0f) * 100).roundToInt())
|
||||
)
|
||||
}
|
||||
BackupStatusData.RestoreStatus.LOW_BATTERY -> stringResource(R.string.BackupStatusRow__download_device_has_low_battery)
|
||||
BackupStatusData.RestoreStatus.WAITING_FOR_INTERNET -> stringResource(R.string.BackupStatusRow__download_no_internet)
|
||||
BackupStatusData.RestoreStatus.WAITING_FOR_WIFI -> stringResource(R.string.BackupStatusRow__download_waiting_for_wifi)
|
||||
BackupStatusData.RestoreStatus.FINISHED -> stringResource(R.string.BackupStatus__restore_complete)
|
||||
RestoreState.CANCELING_MEDIA -> stringResource(R.string.BackupStatus__cancel_restore_media)
|
||||
RestoreState.RESTORING_MEDIA -> {
|
||||
when (backupStatusData.restoreStatus) {
|
||||
RestoreStatus.RESTORING -> {
|
||||
stringResource(
|
||||
R.string.BackupStatusRow__downloading_s_of_s_s,
|
||||
backupStatusData.completedRestoredSize.toUnitString(2),
|
||||
backupStatusData.totalRestoreSize.toUnitString(2),
|
||||
"%d".format(((backupStatusData.progress ?: 0f) * 100).roundToInt())
|
||||
)
|
||||
}
|
||||
RestoreStatus.WAITING_FOR_INTERNET -> stringResource(R.string.BackupStatusRow__download_no_internet)
|
||||
RestoreStatus.WAITING_FOR_WIFI -> stringResource(R.string.BackupStatusRow__download_waiting_for_wifi)
|
||||
RestoreStatus.LOW_BATTERY -> stringResource(R.string.BackupStatusRow__download_device_has_low_battery)
|
||||
RestoreStatus.FINISHED -> stringResource(R.string.BackupStatus__restore_complete)
|
||||
else -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
RestoreState.NONE -> {
|
||||
if (backupStatusData.restoreStatus == RestoreStatus.FINISHED) {
|
||||
stringResource(R.string.BackupStatus__restore_complete)
|
||||
} else {
|
||||
throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
RestoreState.PENDING,
|
||||
RestoreState.RESTORING_DB -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun progressColor(backupStatusData: BackupStatusData): Color {
|
||||
return if (backupStatusData is BackupStatusData.RestoringMedia && backupStatusData.restoreStatus == BackupStatusData.RestoreStatus.NORMAL) {
|
||||
MaterialTheme.colorScheme.primary
|
||||
} else {
|
||||
backupStatusData.iconColors.foreground
|
||||
private fun progressColor(backupStatusData: ArchiveRestoreProgressState): Color {
|
||||
return when (backupStatusData.restoreStatus) {
|
||||
RestoreStatus.RESTORING -> MaterialTheme.colorScheme.primary
|
||||
RestoreStatus.WAITING_FOR_INTERNET,
|
||||
RestoreStatus.WAITING_FOR_WIFI,
|
||||
RestoreStatus.LOW_BATTERY,
|
||||
RestoreStatus.NOT_ENOUGH_DISK_SPACE -> BackupsIconColors.Warning.foreground
|
||||
RestoreStatus.FINISHED -> BackupsIconColors.Success.foreground
|
||||
RestoreStatus.NONE -> BackupsIconColors.Normal.foreground
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,11 +226,7 @@ private fun progressColor(backupStatusData: BackupStatusData): Color {
|
||||
fun BackupStatusRowNormalPreview() {
|
||||
Previews.Preview {
|
||||
BackupStatusRow(
|
||||
backupStatusData = BackupStatusData.RestoringMedia(
|
||||
bytesTotal = ByteSize(100),
|
||||
bytesDownloaded = ByteSize(50),
|
||||
restoreStatus = BackupStatusData.RestoreStatus.NORMAL
|
||||
),
|
||||
backupStatusData = ArchiveRestoreProgressState(restoreState = RestoreState.RESTORING_MEDIA, restoreStatus = RestoreStatus.RESTORING, remainingRestoreSize = 800.mebiBytes, totalRestoreSize = 1024.mebiBytes),
|
||||
onCancelClick = {}
|
||||
)
|
||||
}
|
||||
@@ -284,11 +237,7 @@ fun BackupStatusRowNormalPreview() {
|
||||
fun BackupStatusRowWaitingForWifiPreview() {
|
||||
Previews.Preview {
|
||||
BackupStatusRow(
|
||||
backupStatusData = BackupStatusData.RestoringMedia(
|
||||
bytesTotal = ByteSize(100),
|
||||
bytesDownloaded = ByteSize(50),
|
||||
restoreStatus = BackupStatusData.RestoreStatus.WAITING_FOR_WIFI
|
||||
)
|
||||
backupStatusData = ArchiveRestoreProgressState(restoreState = RestoreState.RESTORING_MEDIA, restoreStatus = RestoreStatus.WAITING_FOR_WIFI, remainingRestoreSize = 800.mebiBytes, totalRestoreSize = 1024.mebiBytes)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -298,11 +247,7 @@ fun BackupStatusRowWaitingForWifiPreview() {
|
||||
fun BackupStatusRowWaitingForInternetPreview() {
|
||||
Previews.Preview {
|
||||
BackupStatusRow(
|
||||
backupStatusData = BackupStatusData.RestoringMedia(
|
||||
bytesTotal = ByteSize(100),
|
||||
bytesDownloaded = ByteSize(50),
|
||||
restoreStatus = BackupStatusData.RestoreStatus.WAITING_FOR_INTERNET
|
||||
)
|
||||
backupStatusData = ArchiveRestoreProgressState(restoreState = RestoreState.RESTORING_MEDIA, restoreStatus = RestoreStatus.WAITING_FOR_INTERNET, remainingRestoreSize = 800.mebiBytes, totalRestoreSize = 1024.mebiBytes)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -312,11 +257,7 @@ fun BackupStatusRowWaitingForInternetPreview() {
|
||||
fun BackupStatusRowLowBatteryPreview() {
|
||||
Previews.Preview {
|
||||
BackupStatusRow(
|
||||
backupStatusData = BackupStatusData.RestoringMedia(
|
||||
bytesTotal = ByteSize(100),
|
||||
bytesDownloaded = ByteSize(50),
|
||||
restoreStatus = BackupStatusData.RestoreStatus.LOW_BATTERY
|
||||
)
|
||||
backupStatusData = ArchiveRestoreProgressState(restoreState = RestoreState.RESTORING_MEDIA, restoreStatus = RestoreStatus.LOW_BATTERY, remainingRestoreSize = 800.mebiBytes, totalRestoreSize = 1024.mebiBytes)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -326,11 +267,8 @@ fun BackupStatusRowLowBatteryPreview() {
|
||||
fun BackupStatusRowFinishedPreview() {
|
||||
Previews.Preview {
|
||||
BackupStatusRow(
|
||||
backupStatusData = BackupStatusData.RestoringMedia(
|
||||
bytesTotal = ByteSize(100),
|
||||
bytesDownloaded = ByteSize(50),
|
||||
restoreStatus = BackupStatusData.RestoreStatus.FINISHED
|
||||
)
|
||||
backupStatusData = ArchiveRestoreProgressState(restoreState = RestoreState.NONE, restoreStatus = RestoreStatus.FINISHED, remainingRestoreSize = 0.mebiBytes, totalRestoreSize = 0.mebiBytes, totalToRestoreThisRun = 1024.mebiBytes),
|
||||
onCancelClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -340,29 +278,7 @@ fun BackupStatusRowFinishedPreview() {
|
||||
fun BackupStatusRowNotEnoughFreeSpacePreview() {
|
||||
Previews.Preview {
|
||||
BackupStatusRow(
|
||||
backupStatusData = BackupStatusData.NotEnoughFreeSpace(
|
||||
requiredSpace = ByteSize(50)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
fun BackupStatusRowCouldNotCompleteBackupPreview() {
|
||||
Previews.Preview {
|
||||
BackupStatusRow(
|
||||
backupStatusData = BackupStatusData.CouldNotCompleteBackup
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
fun BackupStatusRowBackupFailedPreview() {
|
||||
Previews.Preview {
|
||||
BackupStatusRow(
|
||||
backupStatusData = BackupStatusData.BackupFailed
|
||||
backupStatusData = ArchiveRestoreProgressState(restoreState = RestoreState.RESTORING_MEDIA, restoreStatus = RestoreStatus.NOT_ENOUGH_DISK_SPACE, remainingRestoreSize = 500.mebiBytes, totalRestoreSize = 1024.mebiBytes)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,8 @@ fun FilePointer?.toLocalAttachment(
|
||||
contentType: String? = this?.contentType,
|
||||
fileName: String? = this?.fileName,
|
||||
uuid: ByteString? = null,
|
||||
quote: Boolean = false
|
||||
quote: Boolean = false,
|
||||
quoteTargetContentType: String? = null
|
||||
): Attachment? {
|
||||
if (this == null || this.locatorInfo == null) return null
|
||||
|
||||
@@ -71,6 +72,7 @@ fun FilePointer?.toLocalAttachment(
|
||||
stickerLocator = stickerLocator,
|
||||
gif = gif,
|
||||
quote = quote,
|
||||
quoteTargetContentType = quoteTargetContentType,
|
||||
uuid = UuidUtil.fromByteStringOrNull(uuid),
|
||||
fileName = fileName
|
||||
)
|
||||
@@ -100,7 +102,9 @@ fun FilePointer?.toLocalAttachment(
|
||||
PointerAttachment.forPointer(
|
||||
pointer = Optional.of(signalAttachmentPointer),
|
||||
stickerLocator = stickerLocator,
|
||||
transferState = if (wasDownloaded) AttachmentTable.TRANSFER_NEEDS_RESTORE else AttachmentTable.TRANSFER_PROGRESS_PENDING
|
||||
transferState = if (wasDownloaded) AttachmentTable.TRANSFER_NEEDS_RESTORE else AttachmentTable.TRANSFER_PROGRESS_PENDING,
|
||||
quote = quote,
|
||||
quoteTargetContentType = quoteTargetContentType
|
||||
).orNull()
|
||||
}
|
||||
AttachmentType.INVALID -> {
|
||||
@@ -117,6 +121,7 @@ fun FilePointer?.toLocalAttachment(
|
||||
borderless = borderless,
|
||||
gif = gif,
|
||||
quote = quote,
|
||||
quoteTargetContentType = quoteTargetContentType,
|
||||
stickerLocator = stickerLocator,
|
||||
uuid = UuidUtil.fromByteStringOrNull(uuid)
|
||||
)
|
||||
@@ -227,7 +232,7 @@ private fun DatabaseAttachment.toRemoteAttachmentType(): AttachmentType {
|
||||
}
|
||||
|
||||
val activelyOnArchiveCdn = this.archiveTransferState == AttachmentTable.ArchiveTransferState.FINISHED
|
||||
val couldBeOnArchiveCdn = this.transferState == AttachmentTable.TRANSFER_PROGRESS_DONE && this.archiveTransferState != AttachmentTable.ArchiveTransferState.PERMANENT_FAILURE
|
||||
val couldBeOnArchiveCdn = (this.transferState == AttachmentTable.TRANSFER_PROGRESS_DONE || this.transferState == AttachmentTable.TRANSFER_NEEDS_RESTORE) && this.archiveTransferState != AttachmentTable.ArchiveTransferState.PERMANENT_FAILURE
|
||||
|
||||
if (this.dataHash != null && (activelyOnArchiveCdn || couldBeOnArchiveCdn)) {
|
||||
return AttachmentType.ARCHIVE
|
||||
|
||||
@@ -5,143 +5,53 @@
|
||||
|
||||
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 kotlinx.coroutines.flow.filter
|
||||
import org.thoughtcrime.securesms.backup.v2.ArchiveRestoreProgress
|
||||
import org.thoughtcrime.securesms.backup.v2.ArchiveRestoreProgressState
|
||||
import org.thoughtcrime.securesms.backup.v2.ArchiveRestoreProgressState.RestoreStatus
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.status.BackupStatusBanner
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.status.BackupStatusData
|
||||
import org.thoughtcrime.securesms.banner.Banner
|
||||
import org.thoughtcrime.securesms.database.DatabaseObserver
|
||||
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
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class MediaRestoreProgressBanner(private val listener: RestoreProgressBannerListener = EmptyListener) : Banner<BackupStatusData>() {
|
||||
|
||||
private var totalRestoredSize: Long = 0
|
||||
class MediaRestoreProgressBanner(private val listener: RestoreProgressBannerListener = EmptyListener) : Banner<ArchiveRestoreProgressState>() {
|
||||
|
||||
override val enabled: Boolean
|
||||
get() = SignalStore.backup.isMediaRestoreInProgress || totalRestoredSize > 0
|
||||
get() = ArchiveRestoreProgress.state.let { it.restoreState.isMediaRestoreOperation || it.restoreStatus == RestoreStatus.FINISHED }
|
||||
|
||||
override val dataFlow: Flow<BackupStatusData> by lazy {
|
||||
SignalStore
|
||||
.backup
|
||||
.totalRestorableAttachmentSizeFlow
|
||||
.flatMapLatest { size ->
|
||||
when {
|
||||
size > 0 -> {
|
||||
totalRestoredSize = size
|
||||
getActiveRestoreFlow()
|
||||
}
|
||||
|
||||
totalRestoredSize > 0 -> {
|
||||
flowOf(
|
||||
BackupStatusData.RestoringMedia(
|
||||
bytesTotal = totalRestoredSize.bytes,
|
||||
restoreStatus = BackupStatusData.RestoreStatus.FINISHED
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
else -> flowOf(BackupStatusData.RestoringMedia())
|
||||
}
|
||||
override val dataFlow: Flow<ArchiveRestoreProgressState> by lazy {
|
||||
ArchiveRestoreProgress
|
||||
.stateFlow
|
||||
.filter {
|
||||
it.restoreStatus != RestoreStatus.NONE && (it.restoreState.isMediaRestoreOperation || it.restoreStatus == RestoreStatus.FINISHED)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun DisplayBanner(model: BackupStatusData, contentPadding: PaddingValues) {
|
||||
override fun DisplayBanner(model: ArchiveRestoreProgressState, contentPadding: PaddingValues) {
|
||||
BackupStatusBanner(
|
||||
data = model,
|
||||
onBannerClick = listener::onBannerClick,
|
||||
onActionClick = listener::onActionClick,
|
||||
onDismissClick = {
|
||||
totalRestoredSize = 0
|
||||
ArchiveRestoreProgress.clearFinishedStatus()
|
||||
listener.onDismissComplete()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun getActiveRestoreFlow(): Flow<BackupStatusData> {
|
||||
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 {
|
||||
val totalRestoreSize = SignalStore.backup.totalRestorableAttachmentSize
|
||||
val remainingAttachmentSize = SignalDatabase.attachments.getRemainingRestorableAttachmentSize()
|
||||
val completedBytes = totalRestoreSize - remainingAttachmentSize
|
||||
|
||||
when {
|
||||
!WifiConstraint.isMet(AppDependencies.application) && !SignalStore.backup.restoreWithCellular -> BackupStatusData.RestoringMedia(completedBytes.bytes, totalRestoreSize.bytes, BackupStatusData.RestoreStatus.WAITING_FOR_WIFI)
|
||||
!NetworkConstraint.isMet(AppDependencies.application) -> BackupStatusData.RestoringMedia(completedBytes.bytes, totalRestoreSize.bytes, BackupStatusData.RestoreStatus.WAITING_FOR_INTERNET)
|
||||
!BatteryNotLowConstraint.isMet() -> BackupStatusData.RestoringMedia(completedBytes.bytes, totalRestoreSize.bytes, BackupStatusData.RestoreStatus.LOW_BATTERY)
|
||||
else -> {
|
||||
val availableBytes = SignalStore.backup.spaceAvailableOnDiskBytes
|
||||
|
||||
if (availableBytes > -1L && remainingAttachmentSize > availableBytes) {
|
||||
BackupStatusData.NotEnoughFreeSpace(requiredSpace = remainingAttachmentSize.bytes)
|
||||
} else {
|
||||
BackupStatusData.RestoringMedia(completedBytes.bytes, totalRestoreSize.bytes)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.flowOn(Dispatchers.IO)
|
||||
}
|
||||
|
||||
interface RestoreProgressBannerListener {
|
||||
fun onBannerClick()
|
||||
fun onActionClick(data: BackupStatusData)
|
||||
fun onActionClick(data: ArchiveRestoreProgressState)
|
||||
fun onDismissComplete()
|
||||
}
|
||||
|
||||
private object EmptyListener : RestoreProgressBannerListener {
|
||||
override fun onBannerClick() = Unit
|
||||
override fun onActionClick(data: BackupStatusData) = Unit
|
||||
override fun onActionClick(data: ArchiveRestoreProgressState) = Unit
|
||||
override fun onDismissComplete() = Unit
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,7 +219,7 @@ public class InputPanel extends ConstraintLayout
|
||||
@NonNull SlideDeck attachments,
|
||||
@NonNull QuoteModel.Type quoteType)
|
||||
{
|
||||
this.quoteView.setQuote(requestManager, id, author, body, false, attachments, null, quoteType);
|
||||
this.quoteView.setQuote(requestManager, id, author, body, false, attachments, null, quoteType, true);
|
||||
if (listener != null) {
|
||||
this.quoteView.setOnClickListener(v -> listener.onQuoteClicked(id, author.getId()));
|
||||
}
|
||||
@@ -309,7 +309,7 @@ public class InputPanel extends ConstraintLayout
|
||||
quoteView.getAuthor().getId(),
|
||||
quoteView.getBody().toString(),
|
||||
false,
|
||||
quoteView.getAttachments(),
|
||||
quoteView.getAttachment(),
|
||||
quoteView.getMentions(),
|
||||
quoteView.getQuoteType(),
|
||||
quoteView.getBodyRanges()));
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
package org.thoughtcrime.securesms.components
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.util.AttributeSet
|
||||
import android.util.DisplayMetrics
|
||||
import android.view.Surface
|
||||
import android.view.View
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.constraintlayout.widget.Guideline
|
||||
@@ -16,8 +13,8 @@ import androidx.core.view.WindowInsetsCompat
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.window.WindowSizeClass.Companion.getWindowSizeClass
|
||||
|
||||
/**
|
||||
* A specialized [ConstraintLayout] that sets guidelines based on the window insets provided
|
||||
@@ -61,7 +58,6 @@ open class InsetAwareConstraintLayout @JvmOverloads constructor(
|
||||
private val windowInsetsListeners: MutableSet<WindowInsetsListener> = mutableSetOf()
|
||||
private val keyboardStateListeners: MutableSet<KeyboardStateListener> = mutableSetOf()
|
||||
private val keyboardAnimator = KeyboardInsetAnimator()
|
||||
private val displayMetrics = DisplayMetrics()
|
||||
private var overridingKeyboard: Boolean = false
|
||||
private var previousKeyboardHeight: Int = 0
|
||||
private var applyRootInsets: Boolean = false
|
||||
@@ -104,6 +100,16 @@ open class InsetAwareConstraintLayout @JvmOverloads constructor(
|
||||
|
||||
private fun insetTarget(): View = if (applyRootInsets) rootView else this
|
||||
|
||||
fun setApplyRootInsets(useRootInsets: Boolean) {
|
||||
if (applyRootInsets == useRootInsets) {
|
||||
return
|
||||
}
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(insetTarget(), null)
|
||||
applyRootInsets = useRootInsets
|
||||
ViewCompat.setOnApplyWindowInsetsListener(insetTarget(), windowInsetsListener)
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies whether or not window insets should be accounted for when applying
|
||||
* insets. This is useful when choosing whether to display the content in this
|
||||
@@ -222,23 +228,7 @@ open class InsetAwareConstraintLayout @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
private fun isLandscape(): Boolean {
|
||||
val rotation = getDeviceRotation()
|
||||
return rotation == Surface.ROTATION_90
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun getDeviceRotation(): Int {
|
||||
if (isInEditMode) {
|
||||
return Surface.ROTATION_0
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 30) {
|
||||
context.display?.getRealMetrics(displayMetrics)
|
||||
} else {
|
||||
ServiceUtil.getWindowManager(context).defaultDisplay.getRealMetrics(displayMetrics)
|
||||
}
|
||||
|
||||
return if (displayMetrics.widthPixels > displayMetrics.heightPixels) Surface.ROTATION_90 else Surface.ROTATION_0
|
||||
return resources.getWindowSizeClass().isLandscape()
|
||||
}
|
||||
|
||||
private val Guideline?.guidelineEnd: Int
|
||||
|
||||
@@ -47,6 +47,7 @@ import org.thoughtcrime.securesms.util.views.Stub;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public class QuoteView extends ConstraintLayout implements RecipientForeverObserver {
|
||||
|
||||
@@ -200,7 +201,8 @@ public class QuoteView extends ConstraintLayout implements RecipientForeverObser
|
||||
boolean originalMissing,
|
||||
@NonNull SlideDeck attachments,
|
||||
@Nullable String storyReaction,
|
||||
@NonNull QuoteModel.Type quoteType)
|
||||
@NonNull QuoteModel.Type quoteType,
|
||||
boolean composeMode)
|
||||
{
|
||||
if (this.author != null) this.author.removeForeverObserver(this);
|
||||
|
||||
@@ -211,9 +213,19 @@ public class QuoteView extends ConstraintLayout implements RecipientForeverObser
|
||||
this.quoteType = quoteType;
|
||||
|
||||
this.author.observeForever(this);
|
||||
|
||||
Slide slide = attachments.getFirstSlide();
|
||||
|
||||
String quoteTargetContentType;
|
||||
if (composeMode) {
|
||||
quoteTargetContentType = Optional.ofNullable(slide).map(Slide::getContentType).orElse(null);
|
||||
} else {
|
||||
quoteTargetContentType = Optional.ofNullable(slide).map(Slide::getQuoteTargetContentType).orElse(null);
|
||||
}
|
||||
|
||||
setQuoteAuthor(author);
|
||||
setQuoteText(resolveBody(body, quoteType), attachments, originalMissing, storyReaction);
|
||||
setQuoteAttachment(requestManager, body, attachments, originalMissing);
|
||||
setQuoteText(resolveBody(body, quoteType), slide, originalMissing, storyReaction, quoteTargetContentType);
|
||||
setQuoteAttachment(requestManager, body, slide, originalMissing, quoteTargetContentType);
|
||||
setQuoteMissingFooter(originalMissing);
|
||||
applyColorTheme();
|
||||
}
|
||||
@@ -267,9 +279,10 @@ public class QuoteView extends ConstraintLayout implements RecipientForeverObser
|
||||
}
|
||||
|
||||
private void setQuoteText(@Nullable CharSequence body,
|
||||
@NonNull SlideDeck attachments,
|
||||
@Nullable Slide slide,
|
||||
boolean originalMissing,
|
||||
@Nullable String storyReaction)
|
||||
@Nullable String storyReaction,
|
||||
@Nullable String quoteTargetContentType)
|
||||
{
|
||||
if (originalMissing && isStoryReply()) {
|
||||
bodyView.setVisibility(GONE);
|
||||
@@ -316,40 +329,37 @@ public class QuoteView extends ConstraintLayout implements RecipientForeverObser
|
||||
bodyView.setVisibility(GONE);
|
||||
mediaDescriptionText.setVisibility(VISIBLE);
|
||||
|
||||
Slide audioSlide = attachments.getSlides().stream().filter(Slide::hasAudio).findFirst().orElse(null);
|
||||
Slide documentSlide = attachments.getSlides().stream().filter(Slide::hasDocument).findFirst().orElse(null);
|
||||
Slide imageSlide = attachments.getSlides().stream().filter(Slide::hasImage).findFirst().orElse(null);
|
||||
Slide videoSlide = attachments.getSlides().stream().filter(Slide::hasVideo).findFirst().orElse(null);
|
||||
Slide stickerSlide = attachments.getSlides().stream().filter(Slide::hasSticker).findFirst().orElse(null);
|
||||
Slide viewOnceSlide = attachments.getSlides().stream().filter(Slide::hasViewOnce).findFirst().orElse(null);
|
||||
|
||||
// Given that most types have images, we specifically check images last
|
||||
if (viewOnceSlide != null) {
|
||||
if (MediaUtil.isViewOnceType(quoteTargetContentType)) {
|
||||
mediaDescriptionText.setPadding(0, mediaDescriptionText.getPaddingTop(), 0, (int) DimensionUnit.DP.toPixels(8));
|
||||
mediaDescriptionText.setText(R.string.QuoteView_view_once_media);
|
||||
} else if (audioSlide != null) {
|
||||
} else if (MediaUtil.isAudioType(quoteTargetContentType)) {
|
||||
mediaDescriptionText.setPadding(0, mediaDescriptionText.getPaddingTop(), 0, (int) DimensionUnit.DP.toPixels(8));
|
||||
mediaDescriptionText.setText(R.string.QuoteView_audio);
|
||||
} else if (documentSlide != null) {
|
||||
mediaDescriptionText.setVisibility(GONE);
|
||||
} else if (videoSlide != null) {
|
||||
if (videoSlide.isVideoGif()) {
|
||||
} else if (MediaUtil.isVideoType(quoteTargetContentType)) {
|
||||
if (slide != null && slide.isVideoGif()) {
|
||||
mediaDescriptionText.setText(R.string.QuoteView_gif);
|
||||
} else {
|
||||
mediaDescriptionText.setText(R.string.QuoteView_video);
|
||||
}
|
||||
} else if (stickerSlide != null) {
|
||||
} else if (slide != null && slide.hasSticker()) {
|
||||
mediaDescriptionText.setText(R.string.QuoteView_sticker);
|
||||
} else if (imageSlide != null) {
|
||||
if (MediaUtil.isGif(imageSlide.getContentType())) {
|
||||
} else if (MediaUtil.isImageType(quoteTargetContentType)) {
|
||||
if (MediaUtil.isGif(quoteTargetContentType)) {
|
||||
mediaDescriptionText.setText(R.string.QuoteView_gif);
|
||||
} else {
|
||||
mediaDescriptionText.setText(R.string.QuoteView_photo);
|
||||
}
|
||||
} else {
|
||||
mediaDescriptionText.setVisibility(GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private void setQuoteAttachment(@NonNull RequestManager requestManager, @NonNull CharSequence body, @NonNull SlideDeck slideDeck, boolean originalMissing) {
|
||||
private void setQuoteAttachment(@NonNull RequestManager requestManager,
|
||||
@NonNull CharSequence body,
|
||||
@NonNull Slide slide,
|
||||
boolean originalMissing,
|
||||
@Nullable String quoteTargetContentType)
|
||||
{
|
||||
boolean outgoing = messageType != MessageType.INCOMING && messageType != MessageType.STORY_REPLY_INCOMING;
|
||||
boolean preview = messageType == MessageType.PREVIEW || messageType == MessageType.STORY_REPLY_PREVIEW;
|
||||
|
||||
@@ -394,41 +404,49 @@ public class QuoteView extends ConstraintLayout implements RecipientForeverObser
|
||||
return;
|
||||
}
|
||||
|
||||
Slide imageVideoSlide = slideDeck.getSlides().stream().filter(s -> s.hasImage() || s.hasVideo() || s.hasSticker()).findFirst().orElse(null);
|
||||
Slide documentSlide = slideDeck.getSlides().stream().filter(Slide::hasDocument).findFirst().orElse(null);
|
||||
Slide viewOnceSlide = slideDeck.getSlides().stream().filter(Slide::hasViewOnce).findFirst().orElse(null);
|
||||
|
||||
attachmentVideoOVerlayStub.setVisibility(GONE);
|
||||
|
||||
if (viewOnceSlide != null) {
|
||||
thumbnailView.setVisibility(GONE);
|
||||
attachmentNameViewStub.setVisibility(GONE);
|
||||
} else if (imageVideoSlide != null && imageVideoSlide.getUri() != null) {
|
||||
thumbnailView.setVisibility(VISIBLE);
|
||||
attachmentNameViewStub.setVisibility(GONE);
|
||||
|
||||
if (dismissStub.resolved()) {
|
||||
dismissStub.get().setBackgroundResource(R.drawable.dismiss_background);
|
||||
}
|
||||
if (imageVideoSlide.hasVideo() && !imageVideoSlide.isVideoGif()) {
|
||||
attachmentVideoOVerlayStub.setVisibility(VISIBLE);
|
||||
}
|
||||
requestManager.load(new DecryptableUri(imageVideoSlide.getUri()))
|
||||
.centerCrop()
|
||||
.override(thumbWidth, thumbHeight)
|
||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
||||
.into(thumbnailView);
|
||||
} else if (documentSlide != null){
|
||||
thumbnailView.setVisibility(GONE);
|
||||
attachmentNameViewStub.setVisibility(VISIBLE);
|
||||
attachmentNameViewStub.get().setText(documentSlide.getFileName().orElse(""));
|
||||
} else {
|
||||
if (TextUtils.isEmpty(quoteTargetContentType) || slide == null || slide.getUri() == null) {
|
||||
thumbnailView.setVisibility(GONE);
|
||||
attachmentNameViewStub.setVisibility(GONE);
|
||||
|
||||
if (dismissStub.resolved()) {
|
||||
dismissStub.get().setBackground(null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
attachmentVideoOVerlayStub.setVisibility(GONE);
|
||||
|
||||
if (MediaUtil.isViewOnceType(quoteTargetContentType)) {
|
||||
thumbnailView.setVisibility(GONE);
|
||||
attachmentNameViewStub.setVisibility(GONE);
|
||||
} else if (MediaUtil.isImageOrVideoType(quoteTargetContentType)) {
|
||||
thumbnailView.setVisibility(VISIBLE);
|
||||
attachmentNameViewStub.setVisibility(GONE);
|
||||
|
||||
if (dismissStub.resolved()) {
|
||||
dismissStub.get().setBackgroundResource(R.drawable.dismiss_background);
|
||||
}
|
||||
|
||||
if (MediaUtil.isVideoType(quoteTargetContentType) && !slide.isVideoGif()) {
|
||||
attachmentVideoOVerlayStub.setVisibility(VISIBLE);
|
||||
}
|
||||
|
||||
requestManager.load(new DecryptableUri(slide.getUri()))
|
||||
.centerCrop()
|
||||
.override(thumbWidth, thumbHeight)
|
||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
||||
.into(thumbnailView);
|
||||
} else if (MediaUtil.isAudioType(quoteTargetContentType)) {
|
||||
thumbnailView.setVisibility(GONE);
|
||||
attachmentNameViewStub.setVisibility(GONE);
|
||||
|
||||
if (dismissStub.resolved()) {
|
||||
dismissStub.get().setBackground(null);
|
||||
}
|
||||
} else {
|
||||
thumbnailView.setVisibility(GONE);
|
||||
attachmentNameViewStub.setVisibility(VISIBLE);
|
||||
attachmentNameViewStub.get().setText(slide.getFileName().orElse(""));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -464,8 +482,13 @@ public class QuoteView extends ConstraintLayout implements RecipientForeverObser
|
||||
return body;
|
||||
}
|
||||
|
||||
public List<Attachment> getAttachments() {
|
||||
return attachments.asAttachments();
|
||||
public @Nullable Attachment getAttachment() {
|
||||
List<Attachment> converted = attachments.asAttachments();
|
||||
if (converted.size() > 0) {
|
||||
return converted.get(0);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public @NonNull QuoteModel.Type getQuoteType() {
|
||||
|
||||
@@ -30,7 +30,6 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
import androidx.compose.ui.res.colorResource
|
||||
@@ -72,7 +71,7 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.BadgeImag
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.completed.InAppPaymentsBottomSheetDelegate
|
||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||
import org.thoughtcrime.securesms.compose.StatusBarColorNestedScrollConnection
|
||||
import org.thoughtcrime.securesms.compose.rememberStatusBarColorNestedScrollModifier
|
||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
@@ -107,16 +106,11 @@ class AppSettingsFragment : ComposeFragment(), Callbacks {
|
||||
)
|
||||
}
|
||||
|
||||
val nestedScrollConnection = remember {
|
||||
StatusBarColorNestedScrollConnection(requireActivity())
|
||||
}
|
||||
|
||||
AppSettingsContent(
|
||||
self = self!!,
|
||||
state = state!!,
|
||||
bannerManager = bannerManager,
|
||||
callbacks = this,
|
||||
lazyColumnModifier = Modifier.nestedScroll(nestedScrollConnection)
|
||||
callbacks = this
|
||||
)
|
||||
}
|
||||
|
||||
@@ -176,8 +170,7 @@ private fun AppSettingsContent(
|
||||
self: BioRecipientState,
|
||||
state: AppSettingsState,
|
||||
bannerManager: BannerManager,
|
||||
callbacks: Callbacks,
|
||||
lazyColumnModifier: Modifier = Modifier
|
||||
callbacks: Callbacks
|
||||
) {
|
||||
val isRegisteredAndUpToDate by rememberUpdatedState(state.isRegisteredAndUpToDate())
|
||||
|
||||
@@ -193,7 +186,7 @@ private fun AppSettingsContent(
|
||||
bannerManager.Banner()
|
||||
|
||||
LazyColumn(
|
||||
modifier = lazyColumnModifier
|
||||
modifier = rememberStatusBarColorNestedScrollModifier()
|
||||
) {
|
||||
item {
|
||||
BioRow(
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Typeface
|
||||
import android.text.InputType
|
||||
import android.text.method.PasswordTransformationMethod
|
||||
import android.util.DisplayMetrics
|
||||
import android.view.ViewGroup
|
||||
import android.widget.EditText
|
||||
@@ -13,7 +14,6 @@ import androidx.annotation.ColorRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.autofill.HintConstants
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -31,7 +31,6 @@ import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.core.app.DialogCompat
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.fragment.findNavController
|
||||
@@ -47,6 +46,7 @@ import org.signal.core.ui.compose.SignalPreview
|
||||
import org.signal.core.ui.compose.Texts
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||
import org.thoughtcrime.securesms.compose.rememberStatusBarColorNestedScrollModifier
|
||||
import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
@@ -116,10 +116,10 @@ class AccountSettingsFragment : ComposeFragment() {
|
||||
|
||||
changeKeyboard.setOnClickListener {
|
||||
if (pinEditText.inputType and InputType.TYPE_CLASS_NUMBER == 0) {
|
||||
pinEditText.inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_VARIATION_PASSWORD
|
||||
pinEditText.inputType = InputType.TYPE_CLASS_NUMBER
|
||||
changeKeyboard.setIconResource(PinKeyboardType.ALPHA_NUMERIC.iconResource)
|
||||
} else {
|
||||
pinEditText.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
|
||||
pinEditText.inputType = InputType.TYPE_CLASS_TEXT
|
||||
changeKeyboard.setIconResource(PinKeyboardType.NUMERIC.iconResource)
|
||||
}
|
||||
pinEditText.typeface = Typeface.DEFAULT
|
||||
@@ -129,20 +129,19 @@ class AccountSettingsFragment : ComposeFragment() {
|
||||
ViewUtil.focusAndShowKeyboard(pinEditText)
|
||||
}
|
||||
|
||||
ViewCompat.setAutofillHints(pinEditText, HintConstants.AUTOFILL_HINT_PASSWORD)
|
||||
|
||||
when (SignalStore.pin.keyboardType) {
|
||||
PinKeyboardType.NUMERIC -> {
|
||||
pinEditText.inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_VARIATION_PASSWORD
|
||||
pinEditText.inputType = InputType.TYPE_CLASS_NUMBER
|
||||
changeKeyboard.setIconResource(PinKeyboardType.ALPHA_NUMERIC.iconResource)
|
||||
}
|
||||
|
||||
PinKeyboardType.ALPHA_NUMERIC -> {
|
||||
pinEditText.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
|
||||
pinEditText.inputType = InputType.TYPE_CLASS_TEXT
|
||||
changeKeyboard.setIconResource(PinKeyboardType.NUMERIC.iconResource)
|
||||
}
|
||||
}
|
||||
|
||||
pinEditText.transformationMethod = PasswordTransformationMethod.getInstance()
|
||||
pinEditText.addTextChangedListener(object : SimpleTextWatcher() {
|
||||
override fun onTextChanged(text: String) {
|
||||
turnOffButton.isEnabled = text.length >= SvrConstants.MINIMUM_PIN_LENGTH
|
||||
@@ -278,7 +277,10 @@ fun AccountSettingsScreen(
|
||||
navigationIcon = ImageVector.vectorResource(R.drawable.ic_arrow_left_24)
|
||||
) { contentPadding ->
|
||||
LazyColumn(
|
||||
modifier = Modifier.padding(contentPadding).testTag(AccountSettingsTestTags.SCROLLER)
|
||||
modifier = Modifier
|
||||
.padding(contentPadding)
|
||||
.then(rememberStatusBarColorNestedScrollModifier())
|
||||
.testTag(AccountSettingsTestTags.SCROLLER)
|
||||
) {
|
||||
item {
|
||||
Texts.SectionHeader(
|
||||
|
||||
@@ -24,6 +24,7 @@ import org.signal.core.ui.compose.SignalPreview
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.app.appearance.navbar.ChooseNavigationBarStyleFragment
|
||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||
import org.thoughtcrime.securesms.compose.rememberStatusBarColorNestedScrollModifier
|
||||
import org.thoughtcrime.securesms.keyvalue.SettingsValues
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
|
||||
@@ -107,7 +108,9 @@ private fun AppearanceSettingsScreen(
|
||||
navigationIcon = ImageVector.vectorResource(R.drawable.symbol_arrow_start_24)
|
||||
) { paddingValues ->
|
||||
LazyColumn(
|
||||
modifier = Modifier.padding(paddingValues)
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.then(rememberStatusBarColorNestedScrollModifier())
|
||||
) {
|
||||
item {
|
||||
Rows.RadioListRow(
|
||||
|
||||
@@ -63,9 +63,9 @@ class BackupStateObserver(
|
||||
private val backupTierChangedNotifier = MutableSharedFlow<Unit>()
|
||||
|
||||
/**
|
||||
* Called when the value returned by [SignalStore.backup.backupTier] changes.
|
||||
* Called when the backup state likely changed.
|
||||
*/
|
||||
fun notifyBackupTierChanged(scope: CoroutineScope = staticScope) {
|
||||
fun notifyBackupStateChanged(scope: CoroutineScope = staticScope) {
|
||||
Log.d(TAG, "Notifier got a change")
|
||||
scope.launch {
|
||||
backupTierChangedNotifier.emit(Unit)
|
||||
|
||||
@@ -20,7 +20,6 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||
import org.thoughtcrime.securesms.util.Environment
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
class BackupsSettingsViewModel : ViewModel() {
|
||||
@@ -44,7 +43,7 @@ class BackupsSettingsViewModel : ViewModel() {
|
||||
it.copy(
|
||||
backupState = enabledState,
|
||||
lastBackupAt = SignalStore.backup.lastBackupTime.milliseconds,
|
||||
showBackupTierInternalOverride = RemoteConfig.internalUser || Environment.IS_STAGING,
|
||||
showBackupTierInternalOverride = Environment.IS_STAGING,
|
||||
backupTierInternalOverride = SignalStore.backup.backupTierInternalOverride
|
||||
)
|
||||
}
|
||||
@@ -60,6 +59,6 @@ class BackupsSettingsViewModel : ViewModel() {
|
||||
StorageSyncHelper.scheduleSyncForDataChange()
|
||||
}
|
||||
|
||||
BackupStateObserver.notifyBackupTierChanged(scope = viewModelScope)
|
||||
BackupStateObserver.notifyBackupStateChanged(scope = viewModelScope)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,17 +5,13 @@
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.backups.remote
|
||||
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.status.BackupStatusData
|
||||
import org.thoughtcrime.securesms.backup.v2.ArchiveRestoreProgressState
|
||||
|
||||
/**
|
||||
* State container for BackupStatusData, including the enabled state.
|
||||
*/
|
||||
sealed interface BackupRestoreState {
|
||||
data object None : BackupRestoreState
|
||||
data class Ready(
|
||||
val bytes: String
|
||||
) : BackupRestoreState
|
||||
data class FromBackupStatusData(
|
||||
val backupStatusData: BackupStatusData
|
||||
) : BackupRestoreState
|
||||
data class Ready(val bytes: String) : BackupRestoreState
|
||||
data class Restoring(val state: ArchiveRestoreProgressState) : BackupRestoreState
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement.spacedBy
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -30,7 +29,6 @@ import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.BasicAlertDialog
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
@@ -40,7 +38,6 @@ import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
@@ -93,17 +90,19 @@ import org.thoughtcrime.securesms.DevicePinAuthEducationSheet
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.backup.ArchiveUploadProgress
|
||||
import org.thoughtcrime.securesms.backup.DeletionState
|
||||
import org.thoughtcrime.securesms.backup.RestoreState
|
||||
import org.thoughtcrime.securesms.backup.v2.ArchiveRestoreProgressState
|
||||
import org.thoughtcrime.securesms.backup.v2.ArchiveRestoreProgressState.RestoreStatus
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupFrequency
|
||||
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.BackupAlert
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.BackupAlertBottomSheet
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.status.BackupStatusData
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.status.BackupCreateErrorRow
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.status.BackupStatusRow
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.status.RestoreType
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
|
||||
import org.thoughtcrime.securesms.billing.launchManageBackupsSubscription
|
||||
import org.thoughtcrime.securesms.components.compose.BetaHeader
|
||||
import org.thoughtcrime.securesms.components.compose.TextWithBetaLabel
|
||||
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
|
||||
import org.thoughtcrime.securesms.components.settings.app.backups.BackupState
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.MessageBackupsCheckoutLauncher.createBackupsCheckoutLauncher
|
||||
@@ -192,7 +191,7 @@ class RemoteBackupsSettingsFragment : ComposeFragment() {
|
||||
viewModel.requestDialog(RemoteBackupsSettingsState.Dialog.TURN_OFF_AND_DELETE_BACKUPS)
|
||||
}
|
||||
|
||||
override fun onChangeBackupFrequencyClick() {
|
||||
override fun onBackupFrequencyClick() {
|
||||
viewModel.requestDialog(RemoteBackupsSettingsState.Dialog.BACKUP_FREQUENCY)
|
||||
}
|
||||
|
||||
@@ -204,10 +203,6 @@ class RemoteBackupsSettingsFragment : ComposeFragment() {
|
||||
viewModel.requestSnackbar(RemoteBackupsSettingsState.Snackbar.NONE)
|
||||
}
|
||||
|
||||
override fun onSelectBackupsFrequencyChange(newFrequency: BackupFrequency) {
|
||||
viewModel.setBackupsFrequency(newFrequency)
|
||||
}
|
||||
|
||||
override fun onTurnOffAndDeleteBackupsConfirm() {
|
||||
viewModel.turnOffAndDeleteBackups()
|
||||
}
|
||||
@@ -230,7 +225,7 @@ class RemoteBackupsSettingsFragment : ComposeFragment() {
|
||||
}
|
||||
|
||||
override fun onCancelMediaRestore() {
|
||||
viewModel.requestDialog(RemoteBackupsSettingsState.Dialog.CANCEL_MEDIA_RESTORE_PROTECTION)
|
||||
viewModel.cancelMediaRestore()
|
||||
}
|
||||
|
||||
override fun onDisplaySkipMediaRestoreProtectionDialog() {
|
||||
@@ -366,10 +361,9 @@ private interface ContentCallbacks {
|
||||
fun onBackupNowClick() = Unit
|
||||
fun onCancelUploadClick() = Unit
|
||||
fun onTurnOffAndDeleteBackupsClick() = Unit
|
||||
fun onChangeBackupFrequencyClick() = Unit
|
||||
fun onBackupFrequencyClick() = Unit
|
||||
fun onDialogDismissed() = Unit
|
||||
fun onSnackbarDismissed() = Unit
|
||||
fun onSelectBackupsFrequencyChange(newFrequency: BackupFrequency) = Unit
|
||||
fun onTurnOffAndDeleteBackupsConfirm() = Unit
|
||||
fun onViewBackupKeyClick() = Unit
|
||||
fun onStartMediaRestore() = Unit
|
||||
@@ -420,9 +414,9 @@ private fun RemoteBackupsSettingsContent(
|
||||
Scaffold(
|
||||
topBar = {
|
||||
Scaffolds.DefaultTopAppBar(
|
||||
title = stringResource(R.string.RemoteBackupsSettingsFragment__signal_backups),
|
||||
title = stringResource(R.string.RemoteBackupsSettingsFragment__secure_backups),
|
||||
titleContent = { _, title ->
|
||||
TextWithBetaLabel(text = title, textStyle = MaterialTheme.typography.titleLarge)
|
||||
Text(text = title, style = MaterialTheme.typography.titleLarge)
|
||||
},
|
||||
onNavigationClick = contentCallbacks::onNavigationClick,
|
||||
navigationIcon = ImageVector.vectorResource(R.drawable.symbol_arrow_start_24),
|
||||
@@ -521,29 +515,28 @@ private fun RemoteBackupsSettingsContent(
|
||||
)
|
||||
} else if (state.backupsEnabled) {
|
||||
appendBackupDetailsItems(
|
||||
backupState = state.backupState,
|
||||
canViewBackupKey = state.canViewBackupKey,
|
||||
state = state,
|
||||
backupRestoreState = backupRestoreState,
|
||||
backupProgress = backupProgress,
|
||||
canBackupMessagesRun = state.canBackupMessagesJobRun,
|
||||
lastBackupTimestamp = state.lastBackupTimestamp,
|
||||
backupMediaSize = state.backupMediaSize,
|
||||
backupsFrequency = state.backupsFrequency,
|
||||
canBackUpUsingCellular = state.canBackUpUsingCellular,
|
||||
canRestoreUsingCellular = state.canRestoreUsingCellular,
|
||||
canBackUpNow = !state.isOutOfStorageSpace,
|
||||
includeDebuglog = state.includeDebuglog,
|
||||
backupMediaDetails = state.backupMediaDetails,
|
||||
contentCallbacks = contentCallbacks
|
||||
)
|
||||
} else {
|
||||
if (backupRestoreState is BackupRestoreState.FromBackupStatusData) {
|
||||
if (state.showBackupCreateFailedError || state.showBackupCreateCouldNotCompleteError) {
|
||||
item {
|
||||
BackupCreateErrorRow(
|
||||
showCouldNotComplete = state.showBackupCreateCouldNotCompleteError,
|
||||
showBackupFailed = state.showBackupCreateFailedError,
|
||||
onLearnMoreClick = contentCallbacks::onLearnMoreAboutBackupFailure
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (backupRestoreState is BackupRestoreState.Restoring) {
|
||||
item {
|
||||
BackupStatusRow(
|
||||
backupStatusData = backupRestoreState.backupStatusData,
|
||||
backupStatusData = backupRestoreState.state,
|
||||
onCancelClick = contentCallbacks::onCancelMediaRestore,
|
||||
onSkipClick = contentCallbacks::onSkipMediaRestore,
|
||||
onLearnMoreClick = contentCallbacks::onLearnMoreAboutBackupFailure
|
||||
onSkipClick = contentCallbacks::onSkipMediaRestore
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -582,10 +575,11 @@ private fun RemoteBackupsSettingsContent(
|
||||
}
|
||||
|
||||
RemoteBackupsSettingsState.Dialog.BACKUP_FREQUENCY -> {
|
||||
BackupFrequencyDialog(
|
||||
selected = state.backupsFrequency,
|
||||
onSelected = contentCallbacks::onSelectBackupsFrequencyChange,
|
||||
onDismiss = contentCallbacks::onDialogDismissed
|
||||
Dialogs.SimpleAlertDialog(
|
||||
title = stringResource(R.string.RemoteBackupsSettingsFragment__backup_frequency),
|
||||
body = stringResource(R.string.RemoteBackupsSettingsFragment__backup_frequency_dialog_body),
|
||||
confirm = stringResource(android.R.string.ok),
|
||||
onConfirm = contentCallbacks::onDialogDismissed
|
||||
)
|
||||
}
|
||||
|
||||
@@ -674,24 +668,24 @@ private fun ReenableBackupsButton(contentCallbacks: ContentCallbacks) {
|
||||
}
|
||||
|
||||
private fun LazyListScope.appendRestoreFromBackupStatusData(
|
||||
backupRestoreState: BackupRestoreState.FromBackupStatusData,
|
||||
backupRestoreState: BackupRestoreState.Restoring,
|
||||
canRestoreUsingCellular: Boolean,
|
||||
contentCallbacks: ContentCallbacks,
|
||||
isCancelable: Boolean = true
|
||||
) {
|
||||
item {
|
||||
BackupStatusRow(
|
||||
backupStatusData = backupRestoreState.backupStatusData,
|
||||
backupStatusData = backupRestoreState.state,
|
||||
restoreType = if (isCancelable) RestoreType.DOWNLOAD else RestoreType.RESTORE,
|
||||
onCancelClick = if (isCancelable) contentCallbacks::onCancelMediaRestore else null,
|
||||
onSkipClick = contentCallbacks::onDisplaySkipMediaRestoreProtectionDialog,
|
||||
onLearnMoreClick = contentCallbacks::onLearnMoreAboutBackupFailure
|
||||
onSkipClick = contentCallbacks::onDisplaySkipMediaRestoreProtectionDialog
|
||||
)
|
||||
}
|
||||
|
||||
val displayResumeButton = when (val data = backupRestoreState.backupStatusData) {
|
||||
is BackupStatusData.RestoringMedia -> !canRestoreUsingCellular && data.restoreStatus == BackupStatusData.RestoreStatus.WAITING_FOR_WIFI
|
||||
else -> false
|
||||
val displayResumeButton = if (backupRestoreState.state.restoreState == RestoreState.RESTORING_MEDIA) {
|
||||
!canRestoreUsingCellular && backupRestoreState.state.restoreStatus == RestoreStatus.WAITING_FOR_WIFI
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
if (displayResumeButton) {
|
||||
@@ -753,7 +747,7 @@ private fun LazyListScope.appendBackupDeletionItems(
|
||||
)
|
||||
}
|
||||
|
||||
if (backupRestoreState is BackupRestoreState.FromBackupStatusData) {
|
||||
if (backupRestoreState is BackupRestoreState.Restoring) {
|
||||
appendRestoreFromBackupStatusData(
|
||||
backupRestoreState = backupRestoreState,
|
||||
canRestoreUsingCellular = canRestoreUsingCellular,
|
||||
@@ -763,7 +757,9 @@ private fun LazyListScope.appendBackupDeletionItems(
|
||||
} else {
|
||||
item {
|
||||
LinearProgressIndicator(
|
||||
modifier = Modifier.horizontalGutters().fillMaxWidth()
|
||||
modifier = Modifier
|
||||
.horizontalGutters()
|
||||
.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -828,19 +824,9 @@ private fun DescriptionText(
|
||||
}
|
||||
|
||||
private fun LazyListScope.appendBackupDetailsItems(
|
||||
backupState: BackupState,
|
||||
canViewBackupKey: Boolean,
|
||||
state: RemoteBackupsSettingsState,
|
||||
backupRestoreState: BackupRestoreState,
|
||||
backupProgress: ArchiveUploadProgressState?,
|
||||
canBackupMessagesRun: Boolean,
|
||||
lastBackupTimestamp: Long,
|
||||
backupMediaSize: Long,
|
||||
backupsFrequency: BackupFrequency,
|
||||
canBackUpUsingCellular: Boolean,
|
||||
canRestoreUsingCellular: Boolean,
|
||||
canBackUpNow: Boolean,
|
||||
includeDebuglog: Boolean?,
|
||||
backupMediaDetails: RemoteBackupsSettingsState.BackupMediaDetails?,
|
||||
contentCallbacks: ContentCallbacks
|
||||
) {
|
||||
item {
|
||||
@@ -851,45 +837,55 @@ private fun LazyListScope.appendBackupDetailsItems(
|
||||
Texts.SectionHeader(text = stringResource(id = R.string.RemoteBackupsSettingsFragment__backup_details))
|
||||
}
|
||||
|
||||
if (backupMediaDetails != null) {
|
||||
if (state.backupMediaDetails != null) {
|
||||
item {
|
||||
Column(modifier = Modifier.horizontalGutters()) {
|
||||
Text("[Internal Only] Backup Media Details")
|
||||
Text("Awaiting Restore: ${backupMediaDetails.awaitingRestore.toUnitString()}")
|
||||
Text("Offloaded: ${backupMediaDetails.offloaded.toUnitString()}")
|
||||
Text("Awaiting Restore: ${state.backupMediaDetails.awaitingRestore.toUnitString()}")
|
||||
Text("Offloaded: ${state.backupMediaDetails.offloaded.toUnitString()}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (state.showBackupCreateFailedError || state.showBackupCreateCouldNotCompleteError) {
|
||||
item {
|
||||
BackupCreateErrorRow(
|
||||
showCouldNotComplete = state.showBackupCreateCouldNotCompleteError,
|
||||
showBackupFailed = state.showBackupCreateFailedError,
|
||||
onLearnMoreClick = contentCallbacks::onLearnMoreAboutBackupFailure
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (backupRestoreState !is BackupRestoreState.None) {
|
||||
if (backupRestoreState is BackupRestoreState.FromBackupStatusData) {
|
||||
if (backupRestoreState is BackupRestoreState.Restoring) {
|
||||
appendRestoreFromBackupStatusData(
|
||||
backupRestoreState = backupRestoreState,
|
||||
canRestoreUsingCellular = canRestoreUsingCellular,
|
||||
canRestoreUsingCellular = state.canRestoreUsingCellular,
|
||||
contentCallbacks = contentCallbacks
|
||||
)
|
||||
} else if (backupRestoreState is BackupRestoreState.Ready) {
|
||||
item {
|
||||
BackupReadyToDownloadRow(
|
||||
ready = backupRestoreState,
|
||||
backupState = backupState,
|
||||
backupState = state.backupState,
|
||||
onDownloadClick = contentCallbacks::onStartMediaRestore
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (includeDebuglog != null) {
|
||||
if (state.includeDebuglog != null) {
|
||||
item {
|
||||
IncludeDebuglogRow(includeDebuglog) { contentCallbacks.onIncludeDebuglogClick(it) }
|
||||
IncludeDebuglogRow(state.includeDebuglog) { contentCallbacks.onIncludeDebuglogClick(it) }
|
||||
}
|
||||
}
|
||||
|
||||
if (backupProgress == null || backupProgress.state == ArchiveUploadProgressState.State.None || backupProgress.state == ArchiveUploadProgressState.State.UserCanceled) {
|
||||
item {
|
||||
LastBackupRow(
|
||||
lastBackupTimestamp = lastBackupTimestamp,
|
||||
enabled = canBackUpNow,
|
||||
lastBackupTimestamp = state.lastBackupTimestamp,
|
||||
enabled = !state.isOutOfStorageSpace,
|
||||
onBackupNowClick = contentCallbacks::onBackupNowClick
|
||||
)
|
||||
}
|
||||
@@ -897,19 +893,19 @@ private fun LazyListScope.appendBackupDetailsItems(
|
||||
item {
|
||||
InProgressBackupRow(
|
||||
archiveUploadProgressState = backupProgress,
|
||||
canBackupMessagesRun = canBackupMessagesRun,
|
||||
canBackupUsingCellular = canBackUpUsingCellular,
|
||||
canBackupMessagesRun = state.canBackupMessagesJobRun,
|
||||
canBackupUsingCellular = state.canBackUpUsingCellular,
|
||||
cancelArchiveUpload = contentCallbacks::onCancelUploadClick
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (backupState !is BackupState.ActiveFree) {
|
||||
if (state.backupState !is BackupState.ActiveFree) {
|
||||
item {
|
||||
val sizeText = if (backupMediaSize < 0L) {
|
||||
stringResource(R.string.RemoteBackupsSettingsFragment__loading)
|
||||
val sizeText = if (state.backupMediaSize < 0L) {
|
||||
stringResource(R.string.RemoteBackupsSettingsFragment__calculating)
|
||||
} else {
|
||||
backupMediaSize.bytes.toUnitString()
|
||||
state.backupMediaSize.bytes.toUnitString()
|
||||
}
|
||||
|
||||
Rows.TextRow(text = {
|
||||
@@ -920,7 +916,7 @@ private fun LazyListScope.appendBackupDetailsItems(
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
Text(
|
||||
text = backupMediaSize.bytes.toUnitString(),
|
||||
text = sizeText,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
@@ -939,19 +935,19 @@ private fun LazyListScope.appendBackupDetailsItems(
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
Text(
|
||||
text = getTextForFrequency(backupsFrequency = backupsFrequency),
|
||||
text = stringResource(id = R.string.RemoteBackupsSettingsFragment__daily),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
},
|
||||
onClick = contentCallbacks::onChangeBackupFrequencyClick
|
||||
onClick = contentCallbacks::onBackupFrequencyClick
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Rows.ToggleRow(
|
||||
checked = canBackUpUsingCellular,
|
||||
checked = state.canBackUpUsingCellular,
|
||||
text = stringResource(id = R.string.RemoteBackupsSettingsFragment__back_up_using_cellular),
|
||||
onCheckChanged = contentCallbacks::onBackUpUsingCellularClick
|
||||
)
|
||||
@@ -961,7 +957,7 @@ private fun LazyListScope.appendBackupDetailsItems(
|
||||
Rows.TextRow(
|
||||
text = stringResource(R.string.RemoteBackupsSettingsFragment__view_backup_key),
|
||||
onClick = contentCallbacks::onViewBackupKeyClick,
|
||||
enabled = canViewBackupKey
|
||||
enabled = state.canViewBackupKey
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1122,13 +1118,17 @@ private fun OutOfStorageSpaceBlock(
|
||||
Dividers.Default()
|
||||
|
||||
Row(
|
||||
modifier = Modifier.horizontalGutters().padding(vertical = 12.dp)
|
||||
modifier = Modifier
|
||||
.horizontalGutters()
|
||||
.padding(vertical = 12.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.symbol_error_circle_fill_24),
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.padding(top = 4.dp, end = 4.dp, start = 2.dp).size(20.dp)
|
||||
modifier = Modifier
|
||||
.padding(top = 4.dp, end = 4.dp, start = 2.dp)
|
||||
.size(20.dp)
|
||||
)
|
||||
|
||||
Column {
|
||||
@@ -1659,64 +1659,6 @@ private fun ResumeRestoreOverCellularDialog(
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun BackupFrequencyDialog(
|
||||
selected: BackupFrequency,
|
||||
onSelected: (BackupFrequency) -> Unit,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
BasicAlertDialog(
|
||||
onDismissRequest = onDismiss
|
||||
) {
|
||||
Surface(
|
||||
color = Dialogs.Defaults.containerColor,
|
||||
shape = Dialogs.Defaults.shape,
|
||||
shadowElevation = Dialogs.Defaults.TonalElevation
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.RemoteBackupsSettingsFragment__backup_frequency),
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.padding(24.dp)
|
||||
)
|
||||
|
||||
BackupFrequency.entries.forEach {
|
||||
Rows.RadioRow(
|
||||
selected = selected == it,
|
||||
text = getTextForFrequency(backupsFrequency = it),
|
||||
label = when (it) {
|
||||
BackupFrequency.MANUAL -> stringResource(id = R.string.RemoteBackupsSettingsFragment__by_tapping_back_up_now)
|
||||
else -> null
|
||||
},
|
||||
modifier = Modifier
|
||||
.padding(end = 24.dp)
|
||||
.clickable(onClick = {
|
||||
onSelected(it)
|
||||
onDismiss()
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
Box(
|
||||
contentAlignment = Alignment.CenterEnd,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 24.dp)
|
||||
.padding(bottom = 24.dp)
|
||||
) {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(text = stringResource(id = android.R.string.cancel))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BackupReadyToDownloadRow(
|
||||
ready: BackupRestoreState.Ready,
|
||||
@@ -1773,7 +1715,6 @@ private fun RemoteBackupsSettingsContentPreview() {
|
||||
lastBackupTimestamp = -1,
|
||||
canBackUpUsingCellular = false,
|
||||
canRestoreUsingCellular = false,
|
||||
backupsFrequency = BackupFrequency.MANUAL,
|
||||
dialog = RemoteBackupsSettingsState.Dialog.NONE,
|
||||
snackbar = RemoteBackupsSettingsState.Snackbar.NONE,
|
||||
backupMediaSize = 2300000,
|
||||
@@ -1785,7 +1726,7 @@ private fun RemoteBackupsSettingsContentPreview() {
|
||||
),
|
||||
statusBarColorNestedScrollConnection = null,
|
||||
backupDeleteState = DeletionState.NONE,
|
||||
backupRestoreState = BackupRestoreState.FromBackupStatusData(BackupStatusData.CouldNotCompleteBackup),
|
||||
backupRestoreState = BackupRestoreState.None,
|
||||
contentCallbacks = ContentCallbacks.Empty,
|
||||
backupProgress = null
|
||||
)
|
||||
@@ -1802,7 +1743,6 @@ private fun RemoteBackupsSettingsInternalUserContentPreview() {
|
||||
lastBackupTimestamp = -1,
|
||||
canBackUpUsingCellular = false,
|
||||
canRestoreUsingCellular = false,
|
||||
backupsFrequency = BackupFrequency.MANUAL,
|
||||
dialog = RemoteBackupsSettingsState.Dialog.NONE,
|
||||
snackbar = RemoteBackupsSettingsState.Snackbar.NONE,
|
||||
backupMediaSize = 2300000,
|
||||
@@ -1815,7 +1755,7 @@ private fun RemoteBackupsSettingsInternalUserContentPreview() {
|
||||
),
|
||||
statusBarColorNestedScrollConnection = null,
|
||||
backupDeleteState = DeletionState.NONE,
|
||||
backupRestoreState = BackupRestoreState.FromBackupStatusData(BackupStatusData.CouldNotCompleteBackup),
|
||||
backupRestoreState = BackupRestoreState.None,
|
||||
contentCallbacks = ContentCallbacks.Empty,
|
||||
backupProgress = null
|
||||
)
|
||||
@@ -2109,18 +2049,6 @@ private fun SkipDownloadDialogPreview() {
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun BackupFrequencyDialogPreview() {
|
||||
Previews.Preview {
|
||||
BackupFrequencyDialog(
|
||||
selected = BackupFrequency.DAILY,
|
||||
onSelected = {},
|
||||
onDismiss = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun BackupDeletionCardPreview() {
|
||||
@@ -2129,11 +2057,8 @@ private fun BackupDeletionCardPreview() {
|
||||
for (state in DeletionState.entries.filter { it.hasUx() }) {
|
||||
appendBackupDeletionItems(
|
||||
backupDeleteState = state,
|
||||
backupRestoreState = BackupRestoreState.FromBackupStatusData(
|
||||
backupStatusData = BackupStatusData.RestoringMedia(
|
||||
bytesDownloaded = 80.mebiBytes,
|
||||
bytesTotal = 3.gibiBytes
|
||||
)
|
||||
backupRestoreState = BackupRestoreState.Restoring(
|
||||
state = ArchiveRestoreProgressState(restoreState = RestoreState.RESTORING_MEDIA, restoreStatus = RestoreStatus.RESTORING, remainingRestoreSize = 800.mebiBytes, totalRestoreSize = 1024.mebiBytes)
|
||||
),
|
||||
contentCallbacks = ContentCallbacks.Empty,
|
||||
canRestoreUsingCellular = true
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.backups.remote
|
||||
|
||||
import org.signal.core.util.ByteSize
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupFrequency
|
||||
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
||||
import org.thoughtcrime.securesms.components.settings.app.backups.BackupState
|
||||
|
||||
@@ -24,13 +23,14 @@ data class RemoteBackupsSettingsState(
|
||||
val totalAllowedStorageSpace: String = "",
|
||||
val backupState: BackupState,
|
||||
val backupMediaSize: Long = -1L,
|
||||
val backupsFrequency: BackupFrequency = BackupFrequency.DAILY,
|
||||
val lastBackupTimestamp: Long = 0,
|
||||
val dialog: Dialog = Dialog.NONE,
|
||||
val snackbar: Snackbar = Snackbar.NONE,
|
||||
val includeDebuglog: Boolean? = null,
|
||||
val canBackupMessagesJobRun: Boolean = false,
|
||||
val backupMediaDetails: BackupMediaDetails? = null
|
||||
val backupMediaDetails: BackupMediaDetails? = null,
|
||||
val showBackupCreateFailedError: Boolean = false,
|
||||
val showBackupCreateCouldNotCompleteError: Boolean = false
|
||||
) {
|
||||
|
||||
data class BackupMediaDetails(
|
||||
|
||||
@@ -30,11 +30,10 @@ import org.signal.core.util.throttleLatest
|
||||
import org.signal.donations.InAppPaymentType
|
||||
import org.thoughtcrime.securesms.backup.ArchiveUploadProgress
|
||||
import org.thoughtcrime.securesms.backup.DeletionState
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupFrequency
|
||||
import org.thoughtcrime.securesms.backup.v2.ArchiveRestoreProgress
|
||||
import org.thoughtcrime.securesms.backup.v2.ArchiveRestoreProgressState.RestoreStatus
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
||||
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.status.BackupStatusData
|
||||
import org.thoughtcrime.securesms.banner.banners.MediaRestoreProgressBanner
|
||||
import org.thoughtcrime.securesms.components.settings.app.backups.BackupStateObserver
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
@@ -45,7 +44,6 @@ import org.thoughtcrime.securesms.jobmanager.impl.BackupMessagesConstraint
|
||||
import org.thoughtcrime.securesms.jobs.BackupMessagesJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.keyvalue.protos.ArchiveUploadProgressState
|
||||
import org.thoughtcrime.securesms.service.MessageBackupListener
|
||||
import org.thoughtcrime.securesms.util.Environment
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
@@ -70,10 +68,11 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
|
||||
canBackupMessagesJobRun = BackupMessagesConstraint.isMet(AppDependencies.application),
|
||||
canViewBackupKey = !TextSecurePreferences.isUnauthorizedReceived(AppDependencies.application),
|
||||
lastBackupTimestamp = SignalStore.backup.lastBackupTime,
|
||||
backupsFrequency = SignalStore.backup.backupFrequency,
|
||||
canBackUpUsingCellular = SignalStore.backup.backupWithCellular,
|
||||
canRestoreUsingCellular = SignalStore.backup.restoreWithCellular,
|
||||
includeDebuglog = SignalStore.internal.includeDebuglogInBackup.takeIf { RemoteConfig.internalUser }
|
||||
includeDebuglog = SignalStore.internal.includeDebuglogInBackup.takeIf { RemoteConfig.internalUser },
|
||||
showBackupCreateFailedError = BackupRepository.shouldDisplayBackupFailedSettingsRow(),
|
||||
showBackupCreateCouldNotCompleteError = BackupRepository.shouldDisplayCouldNotCompleteBackupSettingsRow()
|
||||
)
|
||||
)
|
||||
|
||||
@@ -114,16 +113,14 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val restoreProgress = MediaRestoreProgressBanner()
|
||||
|
||||
var optimizedRemainingBytes = 0L
|
||||
while (isActive) {
|
||||
if (restoreProgress.enabled) {
|
||||
if (ArchiveRestoreProgress.state.let { it.restoreState.isMediaRestoreOperation || it.restoreStatus == RestoreStatus.FINISHED }) {
|
||||
Log.d(TAG, "Backup is being restored. Collecting updates.")
|
||||
restoreProgress
|
||||
.dataFlow
|
||||
.onEach { latest -> _restoreState.update { BackupRestoreState.FromBackupStatusData(latest) } }
|
||||
.takeWhile { it !is BackupStatusData.RestoringMedia || it.restoreStatus != BackupStatusData.RestoreStatus.FINISHED }
|
||||
ArchiveRestoreProgress
|
||||
.stateFlow
|
||||
.takeWhile { it.restoreState.isMediaRestoreOperation || it.restoreStatus == RestoreStatus.FINISHED }
|
||||
.onEach { latest -> _restoreState.update { BackupRestoreState.Restoring(latest) } }
|
||||
.collect()
|
||||
} else if (
|
||||
!SignalStore.backup.optimizeStorage &&
|
||||
@@ -133,10 +130,6 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
|
||||
_restoreState.update { BackupRestoreState.Ready(optimizedRemainingBytes.bytes.toUnitString()) }
|
||||
} else if (SignalStore.backup.totalRestorableAttachmentSize > 0L) {
|
||||
_restoreState.update { BackupRestoreState.Ready(SignalStore.backup.totalRestorableAttachmentSize.bytes.toUnitString()) }
|
||||
} else if (BackupRepository.shouldDisplayBackupFailedSettingsRow()) {
|
||||
_restoreState.update { BackupRestoreState.FromBackupStatusData(BackupStatusData.BackupFailed) }
|
||||
} else if (BackupRepository.shouldDisplayCouldNotCompleteBackupSettingsRow()) {
|
||||
_restoreState.update { BackupRestoreState.FromBackupStatusData(BackupStatusData.CouldNotCompleteBackup) }
|
||||
} else {
|
||||
_restoreState.update { BackupRestoreState.None }
|
||||
}
|
||||
@@ -164,6 +157,10 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
BackupRepository.maybeFixAnyDanglingUploadProgress()
|
||||
}
|
||||
}
|
||||
|
||||
fun setCanBackUpUsingCellular(canBackUpUsingCellular: Boolean) {
|
||||
@@ -181,17 +178,18 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
|
||||
_state.update { it.copy(canRestoreUsingCellular = true) }
|
||||
}
|
||||
|
||||
fun setBackupsFrequency(backupsFrequency: BackupFrequency) {
|
||||
SignalStore.backup.backupFrequency = backupsFrequency
|
||||
_state.update { it.copy(backupsFrequency = backupsFrequency) }
|
||||
MessageBackupListener.setNextBackupTimeToIntervalFromNow()
|
||||
MessageBackupListener.schedule(AppDependencies.application)
|
||||
}
|
||||
|
||||
fun beginMediaRestore() {
|
||||
BackupRepository.resumeMediaRestore()
|
||||
}
|
||||
|
||||
fun cancelMediaRestore() {
|
||||
if (ArchiveRestoreProgress.state.restoreStatus == RestoreStatus.FINISHED) {
|
||||
ArchiveRestoreProgress.clearFinishedStatus()
|
||||
} else {
|
||||
requestDialog(RemoteBackupsSettingsState.Dialog.CANCEL_MEDIA_RESTORE_PROTECTION)
|
||||
}
|
||||
}
|
||||
|
||||
fun skipMediaRestore() {
|
||||
BackupRepository.skipMediaRestore()
|
||||
|
||||
@@ -298,11 +296,12 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
|
||||
lastBackupTimestamp = SignalStore.backup.lastBackupTime,
|
||||
canBackupMessagesJobRun = BackupMessagesConstraint.isMet(AppDependencies.application),
|
||||
backupMediaSize = getBackupMediaSize(),
|
||||
backupsFrequency = SignalStore.backup.backupFrequency,
|
||||
canBackUpUsingCellular = SignalStore.backup.backupWithCellular,
|
||||
canRestoreUsingCellular = SignalStore.backup.restoreWithCellular,
|
||||
isOutOfStorageSpace = BackupRepository.shouldDisplayOutOfRemoteStorageSpaceUx(),
|
||||
hasRedemptionError = lastPurchase?.data?.error?.data_ == "409"
|
||||
hasRedemptionError = lastPurchase?.data?.error?.data_ == "409",
|
||||
showBackupCreateFailedError = BackupRepository.shouldDisplayBackupFailedSettingsRow(),
|
||||
showBackupCreateCouldNotCompleteError = BackupRepository.shouldDisplayCouldNotCompleteBackupSettingsRow()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ package org.thoughtcrime.securesms.components.settings.app.changenumber
|
||||
|
||||
import android.os.Bundle
|
||||
import android.text.InputType
|
||||
import android.text.method.PasswordTransformationMethod
|
||||
import android.view.KeyEvent
|
||||
import android.view.View
|
||||
import android.view.inputmethod.EditorInfo
|
||||
@@ -287,11 +288,12 @@ class ChangeNumberRegistrationLockFragment : LoggingFragment(R.layout.fragment_c
|
||||
val isAlphaNumeric = keyboard == PinKeyboardType.ALPHA_NUMERIC
|
||||
|
||||
binding.kbsLockPinInput.setInputType(
|
||||
if (isAlphaNumeric) InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
|
||||
else InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_VARIATION_PASSWORD
|
||||
if (isAlphaNumeric) InputType.TYPE_CLASS_TEXT
|
||||
else InputType.TYPE_CLASS_NUMBER
|
||||
)
|
||||
|
||||
binding.kbsLockPinInput.getText().clear()
|
||||
binding.kbsLockPinInput.transformationMethod = PasswordTransformationMethod.getInstance()
|
||||
}
|
||||
|
||||
private fun navigateToAccountLocked() {
|
||||
|
||||
@@ -1,125 +1,247 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.chats
|
||||
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.Navigation
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.signal.core.ui.compose.Dividers
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.Rows
|
||||
import org.signal.core.ui.compose.Scaffolds
|
||||
import org.signal.core.ui.compose.SignalPreview
|
||||
import org.signal.core.ui.compose.Texts
|
||||
import org.thoughtcrime.securesms.R
|
||||
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.configure
|
||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||
import org.thoughtcrime.securesms.compose.rememberStatusBarColorNestedScrollModifier
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
|
||||
class ChatsSettingsFragment : DSLSettingsFragment(R.string.preferences_chats__chats) {
|
||||
/**
|
||||
* Displays a list of chats settings options to the user, including
|
||||
* generating link previews and keeping muted chats archived.
|
||||
*/
|
||||
class ChatsSettingsFragment : ComposeFragment() {
|
||||
|
||||
private lateinit var viewModel: ChatsSettingsViewModel
|
||||
private val viewModel: ChatsSettingsViewModel by viewModels()
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
viewModel.refresh()
|
||||
}
|
||||
|
||||
@Suppress("ReplaceGetOrSet")
|
||||
override fun bindAdapter(adapter: MappingAdapter) {
|
||||
viewModel = ViewModelProvider(this).get(ChatsSettingsViewModel::class.java)
|
||||
@Composable
|
||||
override fun FragmentContent() {
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
val callbacks = remember { Callbacks() }
|
||||
|
||||
viewModel.state.observe(viewLifecycleOwner) {
|
||||
adapter.submitList(getConfiguration(it).toMappingModelList())
|
||||
}
|
||||
ChatsSettingsScreen(
|
||||
state = state,
|
||||
callbacks = callbacks,
|
||||
isRemoteBackupsAvailable = RemoteConfig.messageBackups
|
||||
)
|
||||
}
|
||||
|
||||
private fun getConfiguration(state: ChatsSettingsState): DSLConfiguration {
|
||||
return configure {
|
||||
switchPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__generate_link_previews),
|
||||
summary = DSLSettingsText.from(R.string.preferences__retrieve_link_previews_from_websites_for_messages),
|
||||
isEnabled = state.isRegisteredAndUpToDate(),
|
||||
isChecked = state.generateLinkPreviews,
|
||||
onClick = {
|
||||
viewModel.setGenerateLinkPreviewsEnabled(!state.generateLinkPreviews)
|
||||
}
|
||||
)
|
||||
private inner class Callbacks : ChatsSettingsCallbacks {
|
||||
override fun onNavigationClick() {
|
||||
requireActivity().onBackPressedDispatcher.onBackPressed()
|
||||
}
|
||||
|
||||
switchPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__pref_use_address_book_photos),
|
||||
summary = DSLSettingsText.from(R.string.preferences__display_contact_photos_from_your_address_book_if_available),
|
||||
isEnabled = state.isRegisteredAndUpToDate(),
|
||||
isChecked = state.useAddressBook,
|
||||
onClick = {
|
||||
viewModel.setUseAddressBook(!state.useAddressBook)
|
||||
}
|
||||
)
|
||||
override fun onGenerateLinkPreviewsChanged(enabled: Boolean) {
|
||||
viewModel.setGenerateLinkPreviewsEnabled(enabled)
|
||||
}
|
||||
|
||||
switchPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__pref_keep_muted_chats_archived),
|
||||
summary = DSLSettingsText.from(R.string.preferences__muted_chats_that_are_archived_will_remain_archived),
|
||||
isEnabled = state.isRegisteredAndUpToDate(),
|
||||
isChecked = state.keepMutedChatsArchived,
|
||||
onClick = {
|
||||
viewModel.setKeepMutedChatsArchived(!state.keepMutedChatsArchived)
|
||||
}
|
||||
)
|
||||
override fun onUseAddressBookChanged(enabled: Boolean) {
|
||||
viewModel.setUseAddressBook(enabled)
|
||||
}
|
||||
|
||||
dividerPref()
|
||||
override fun onKeepMutedChatsArchivedChanged(enabled: Boolean) {
|
||||
viewModel.setKeepMutedChatsArchived(enabled)
|
||||
}
|
||||
|
||||
sectionHeaderPref(R.string.ChatsSettingsFragment__chat_folders)
|
||||
override fun onAddAChatFolderClick() {
|
||||
findNavController().safeNavigate(R.id.action_chatsSettingsFragment_to_chatFoldersFragment)
|
||||
}
|
||||
|
||||
if (state.folderCount == 1) {
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.ChatsSettingsFragment__add_chat_folder),
|
||||
isEnabled = state.isRegisteredAndUpToDate(),
|
||||
onClick = {
|
||||
Navigation.findNavController(requireView()).safeNavigate(R.id.action_chatsSettingsFragment_to_chatFoldersFragment)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.ChatsSettingsFragment__add_edit_chat_folder),
|
||||
summary = DSLSettingsText.from(resources.getQuantityString(R.plurals.ChatsSettingsFragment__d_folder, state.folderCount, state.folderCount)),
|
||||
isEnabled = state.isRegisteredAndUpToDate(),
|
||||
onClick = {
|
||||
Navigation.findNavController(requireView()).safeNavigate(R.id.action_chatsSettingsFragment_to_chatFoldersFragment)
|
||||
}
|
||||
override fun onAddOrEditFoldersClick() {
|
||||
findNavController().safeNavigate(R.id.action_chatsSettingsFragment_to_chatFoldersFragment)
|
||||
}
|
||||
|
||||
override fun onUseSystemEmojiChanged(enabled: Boolean) {
|
||||
viewModel.setUseSystemEmoji(enabled)
|
||||
}
|
||||
|
||||
override fun onEnterKeySendsChanged(enabled: Boolean) {
|
||||
viewModel.setEnterKeySends(enabled)
|
||||
}
|
||||
|
||||
override fun onChatBackupsClick() {
|
||||
findNavController().safeNavigate(R.id.action_chatsSettingsFragment_to_backupsPreferenceFragment)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private interface ChatsSettingsCallbacks {
|
||||
fun onNavigationClick() = Unit
|
||||
fun onGenerateLinkPreviewsChanged(enabled: Boolean) = Unit
|
||||
fun onUseAddressBookChanged(enabled: Boolean) = Unit
|
||||
fun onKeepMutedChatsArchivedChanged(enabled: Boolean) = Unit
|
||||
fun onAddAChatFolderClick() = Unit
|
||||
fun onAddOrEditFoldersClick() = Unit
|
||||
fun onUseSystemEmojiChanged(enabled: Boolean) = Unit
|
||||
fun onEnterKeySendsChanged(enabled: Boolean) = Unit
|
||||
fun onChatBackupsClick() = Unit
|
||||
|
||||
object Empty : ChatsSettingsCallbacks
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChatsSettingsScreen(
|
||||
isRemoteBackupsAvailable: Boolean,
|
||||
state: ChatsSettingsState,
|
||||
callbacks: ChatsSettingsCallbacks
|
||||
) {
|
||||
Scaffolds.Settings(
|
||||
title = stringResource(R.string.preferences_chats__chats),
|
||||
onNavigationClick = callbacks::onNavigationClick,
|
||||
navigationIcon = ImageVector.vectorResource(R.drawable.symbol_arrow_start_24)
|
||||
) { paddingValues ->
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.then(rememberStatusBarColorNestedScrollModifier())
|
||||
) {
|
||||
item {
|
||||
Rows.ToggleRow(
|
||||
text = stringResource(R.string.preferences__generate_link_previews),
|
||||
label = stringResource(R.string.preferences__retrieve_link_previews_from_websites_for_messages),
|
||||
enabled = state.isRegisteredAndUpToDate(),
|
||||
checked = state.generateLinkPreviews,
|
||||
onCheckChanged = callbacks::onGenerateLinkPreviewsChanged
|
||||
)
|
||||
}
|
||||
|
||||
dividerPref()
|
||||
|
||||
sectionHeaderPref(R.string.ChatsSettingsFragment__keyboard)
|
||||
|
||||
switchPref(
|
||||
title = DSLSettingsText.from(R.string.preferences_advanced__use_system_emoji),
|
||||
isEnabled = state.isRegisteredAndUpToDate(),
|
||||
isChecked = state.useSystemEmoji,
|
||||
onClick = {
|
||||
viewModel.setUseSystemEmoji(!state.useSystemEmoji)
|
||||
}
|
||||
)
|
||||
|
||||
switchPref(
|
||||
title = DSLSettingsText.from(R.string.ChatsSettingsFragment__send_with_enter),
|
||||
isEnabled = state.isRegisteredAndUpToDate(),
|
||||
isChecked = state.enterKeySends,
|
||||
onClick = {
|
||||
viewModel.setEnterKeySends(!state.enterKeySends)
|
||||
}
|
||||
)
|
||||
|
||||
if (!RemoteConfig.messageBackups) {
|
||||
dividerPref()
|
||||
|
||||
sectionHeaderPref(R.string.preferences_chats__backups)
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.preferences_chats__chat_backups),
|
||||
summary = DSLSettingsText.from(if (state.localBackupsEnabled) R.string.arrays__enabled else R.string.arrays__disabled),
|
||||
isEnabled = state.localBackupsEnabled || state.isRegisteredAndUpToDate(),
|
||||
onClick = {
|
||||
Navigation.findNavController(requireView()).safeNavigate(R.id.action_chatsSettingsFragment_to_backupsPreferenceFragment)
|
||||
}
|
||||
item {
|
||||
Rows.ToggleRow(
|
||||
text = stringResource(R.string.preferences__pref_use_address_book_photos),
|
||||
label = stringResource(R.string.preferences__display_contact_photos_from_your_address_book_if_available),
|
||||
enabled = state.isRegisteredAndUpToDate(),
|
||||
checked = state.useAddressBook,
|
||||
onCheckChanged = callbacks::onUseAddressBookChanged
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Rows.ToggleRow(
|
||||
text = stringResource(R.string.preferences__pref_keep_muted_chats_archived),
|
||||
label = stringResource(R.string.preferences__muted_chats_that_are_archived_will_remain_archived),
|
||||
enabled = state.isRegisteredAndUpToDate(),
|
||||
checked = state.keepMutedChatsArchived,
|
||||
onCheckChanged = callbacks::onKeepMutedChatsArchivedChanged
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Dividers.Default()
|
||||
}
|
||||
|
||||
item {
|
||||
Texts.SectionHeader(stringResource(R.string.ChatsSettingsFragment__chat_folders))
|
||||
}
|
||||
|
||||
if (state.folderCount == 1) {
|
||||
item {
|
||||
Rows.TextRow(
|
||||
text = stringResource(R.string.ChatsSettingsFragment__add_chat_folder),
|
||||
enabled = state.isRegisteredAndUpToDate(),
|
||||
onClick = callbacks::onAddAChatFolderClick
|
||||
)
|
||||
}
|
||||
} else {
|
||||
item {
|
||||
Rows.TextRow(
|
||||
text = stringResource(R.string.ChatsSettingsFragment__add_edit_chat_folder),
|
||||
label = pluralStringResource(R.plurals.ChatsSettingsFragment__d_folder, state.folderCount, state.folderCount),
|
||||
enabled = state.isRegisteredAndUpToDate(),
|
||||
onClick = callbacks::onAddOrEditFoldersClick
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Dividers.Default()
|
||||
}
|
||||
|
||||
item {
|
||||
Texts.SectionHeader(stringResource(R.string.ChatsSettingsFragment__keyboard))
|
||||
}
|
||||
|
||||
item {
|
||||
Rows.ToggleRow(
|
||||
text = stringResource(R.string.preferences_advanced__use_system_emoji),
|
||||
enabled = state.isRegisteredAndUpToDate(),
|
||||
checked = state.useSystemEmoji,
|
||||
onCheckChanged = callbacks::onUseSystemEmojiChanged
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Rows.ToggleRow(
|
||||
text = stringResource(R.string.ChatsSettingsFragment__send_with_enter),
|
||||
enabled = state.isRegisteredAndUpToDate(),
|
||||
checked = state.enterKeySends,
|
||||
onCheckChanged = callbacks::onEnterKeySendsChanged
|
||||
)
|
||||
}
|
||||
|
||||
if (!isRemoteBackupsAvailable) {
|
||||
item {
|
||||
Dividers.Default()
|
||||
}
|
||||
|
||||
item {
|
||||
Texts.SectionHeader(stringResource(R.string.preferences_chats__backups))
|
||||
}
|
||||
|
||||
item {
|
||||
Rows.TextRow(
|
||||
text = stringResource(R.string.preferences_chats__chat_backups),
|
||||
label = stringResource(if (state.localBackupsEnabled) R.string.arrays__enabled else R.string.arrays__disabled),
|
||||
enabled = state.localBackupsEnabled || state.isRegisteredAndUpToDate(),
|
||||
onClick = callbacks::onChatBackupsClick
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun ChatsSettingsScreenPreview() {
|
||||
Previews.Preview {
|
||||
ChatsSettingsScreen(
|
||||
state = ChatsSettingsState(
|
||||
generateLinkPreviews = true,
|
||||
useAddressBook = true,
|
||||
keepMutedChatsArchived = true,
|
||||
useSystemEmoji = false,
|
||||
enterKeySends = false,
|
||||
localBackupsEnabled = true,
|
||||
folderCount = 1,
|
||||
userUnregistered = false,
|
||||
clientDeprecated = false
|
||||
),
|
||||
callbacks = ChatsSettingsCallbacks.Empty,
|
||||
isRemoteBackupsAvailable = false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.chats
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import org.thoughtcrime.securesms.components.settings.app.chats.folders.ChatFoldersRepository
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
@@ -12,7 +14,6 @@ import org.thoughtcrime.securesms.util.BackupUtil
|
||||
import org.thoughtcrime.securesms.util.ConversationUtil
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.thoughtcrime.securesms.util.ThrottledDebouncer
|
||||
import org.thoughtcrime.securesms.util.livedata.Store
|
||||
|
||||
class ChatsSettingsViewModel @JvmOverloads constructor(
|
||||
private val repository: ChatsSettingsRepository = ChatsSettingsRepository()
|
||||
@@ -20,7 +21,7 @@ class ChatsSettingsViewModel @JvmOverloads constructor(
|
||||
|
||||
private val refreshDebouncer = ThrottledDebouncer(500L)
|
||||
|
||||
private val store: Store<ChatsSettingsState> = Store(
|
||||
private val store = MutableStateFlow(
|
||||
ChatsSettingsState(
|
||||
generateLinkPreviews = SignalStore.settings.isLinkPreviewsEnabled,
|
||||
useAddressBook = SignalStore.settings.isPreferSystemContactPhotos,
|
||||
@@ -34,7 +35,7 @@ class ChatsSettingsViewModel @JvmOverloads constructor(
|
||||
)
|
||||
)
|
||||
|
||||
val state: LiveData<ChatsSettingsState> = store.stateLiveData
|
||||
val state: StateFlow<ChatsSettingsState> = store
|
||||
|
||||
fun setGenerateLinkPreviewsEnabled(enabled: Boolean) {
|
||||
store.update { it.copy(generateLinkPreviews = enabled) }
|
||||
@@ -70,7 +71,7 @@ class ChatsSettingsViewModel @JvmOverloads constructor(
|
||||
val count = ChatFoldersRepository.getFolderCount()
|
||||
val backupsEnabled = SignalStore.settings.isBackupEnabled && BackupUtil.canUserAccessBackupDirectory(AppDependencies.application)
|
||||
|
||||
if (store.state.localBackupsEnabled != backupsEnabled) {
|
||||
if (store.value.localBackupsEnabled != backupsEnabled) {
|
||||
store.update {
|
||||
it.copy(
|
||||
folderCount = count,
|
||||
|
||||
@@ -1,134 +1,269 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.data
|
||||
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.Navigation
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringArrayResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.preference.PreferenceManager
|
||||
import org.signal.core.ui.compose.Dividers
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.Rows
|
||||
import org.signal.core.ui.compose.Scaffolds
|
||||
import org.signal.core.ui.compose.SignalPreview
|
||||
import org.signal.core.ui.compose.Texts
|
||||
import org.signal.core.util.bytes
|
||||
import org.thoughtcrime.securesms.R
|
||||
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.configure
|
||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||
import org.thoughtcrime.securesms.compose.rememberStatusBarColorNestedScrollModifier
|
||||
import org.thoughtcrime.securesms.mms.SentMediaQuality
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
import org.thoughtcrime.securesms.webrtc.CallDataMode
|
||||
import kotlin.math.abs
|
||||
|
||||
class DataAndStorageSettingsFragment : DSLSettingsFragment(R.string.preferences__data_and_storage) {
|
||||
class DataAndStorageSettingsFragment : ComposeFragment() {
|
||||
|
||||
private val autoDownloadValues by lazy { resources.getStringArray(R.array.pref_media_download_entries) }
|
||||
private val autoDownloadLabels by lazy { resources.getStringArray(R.array.pref_media_download_values) }
|
||||
|
||||
private val sentMediaQualityLabels by lazy { SentMediaQuality.getLabels(requireContext()) }
|
||||
|
||||
private val callDataModeLabels by lazy { resources.getStringArray(R.array.pref_data_and_storage_call_data_mode_values) }
|
||||
|
||||
private lateinit var viewModel: DataAndStorageSettingsViewModel
|
||||
private val viewModel: DataAndStorageSettingsViewModel by viewModels(
|
||||
factoryProducer = {
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
val repository = DataAndStorageSettingsRepository()
|
||||
DataAndStorageSettingsViewModel.Factory(preferences, repository)
|
||||
}
|
||||
)
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
viewModel.refresh()
|
||||
}
|
||||
|
||||
override fun bindAdapter(adapter: MappingAdapter) {
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
val repository = DataAndStorageSettingsRepository()
|
||||
val factory = DataAndStorageSettingsViewModel.Factory(preferences, repository)
|
||||
viewModel = ViewModelProvider(this, factory)[DataAndStorageSettingsViewModel::class.java]
|
||||
@Composable
|
||||
override fun FragmentContent() {
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
val callbacks = remember { Callbacks() }
|
||||
|
||||
viewModel.state.observe(viewLifecycleOwner) {
|
||||
adapter.submitList(getConfiguration(it).toMappingModelList())
|
||||
}
|
||||
DataAndStorageSettingsScreen(
|
||||
state = state,
|
||||
callbacks = callbacks
|
||||
)
|
||||
}
|
||||
|
||||
fun getConfiguration(state: DataAndStorageSettingsState): DSLConfiguration {
|
||||
return configure {
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.preferences_data_and_storage__manage_storage),
|
||||
summary = DSLSettingsText.from(state.totalStorageUse.bytes.toUnitString()),
|
||||
onClick = {
|
||||
Navigation.findNavController(requireView()).safeNavigate(R.id.action_dataAndStorageSettingsFragment_to_storagePreferenceFragment)
|
||||
}
|
||||
)
|
||||
private inner class Callbacks : DataAndStorageSettingsCallbacks {
|
||||
override fun onNavigationClick() {
|
||||
requireActivity().onBackPressedDispatcher.onBackPressed()
|
||||
}
|
||||
|
||||
dividerPref()
|
||||
override fun onManageStorageClick() {
|
||||
findNavController().safeNavigate(R.id.action_dataAndStorageSettingsFragment_to_storagePreferenceFragment)
|
||||
}
|
||||
|
||||
sectionHeaderPref(R.string.preferences_chats__media_auto_download)
|
||||
override fun onSentMediaQualitySelected(code: String) {
|
||||
viewModel.setSentMediaQuality(SentMediaQuality.fromCode(code.toInt()))
|
||||
}
|
||||
|
||||
multiSelectPref(
|
||||
title = DSLSettingsText.from(R.string.preferences_chats__when_using_mobile_data),
|
||||
listItems = autoDownloadLabels,
|
||||
selected = autoDownloadValues.map { state.mobileAutoDownloadValues.contains(it) }.toBooleanArray(),
|
||||
onSelected = {
|
||||
val resultSet = it.mapIndexed { index, selected -> if (selected) autoDownloadValues[index] else null }.filterNotNull().toSet()
|
||||
viewModel.setMobileAutoDownloadValues(resultSet)
|
||||
}
|
||||
)
|
||||
override fun onCallDataModeSelected(code: String) {
|
||||
viewModel.setCallDataMode(CallDataMode.fromCode(abs(code.toInt() - 2)))
|
||||
}
|
||||
|
||||
multiSelectPref(
|
||||
title = DSLSettingsText.from(R.string.preferences_chats__when_using_wifi),
|
||||
listItems = autoDownloadLabels,
|
||||
selected = autoDownloadValues.map { state.wifiAutoDownloadValues.contains(it) }.toBooleanArray(),
|
||||
onSelected = {
|
||||
val resultSet = it.mapIndexed { index, selected -> if (selected) autoDownloadValues[index] else null }.filterNotNull().toSet()
|
||||
viewModel.setWifiAutoDownloadValues(resultSet)
|
||||
}
|
||||
)
|
||||
override fun onUseProxyClick() {
|
||||
findNavController().safeNavigate(R.id.action_dataAndStorageSettingsFragment_to_editProxyFragment)
|
||||
}
|
||||
|
||||
multiSelectPref(
|
||||
title = DSLSettingsText.from(R.string.preferences_chats__when_roaming),
|
||||
listItems = autoDownloadLabels,
|
||||
selected = autoDownloadValues.map { state.roamingAutoDownloadValues.contains(it) }.toBooleanArray(),
|
||||
onSelected = {
|
||||
val resultSet = it.mapIndexed { index, selected -> if (selected) autoDownloadValues[index] else null }.filterNotNull().toSet()
|
||||
viewModel.setRoamingAutoDownloadValues(resultSet)
|
||||
}
|
||||
)
|
||||
override fun onMobileDataAutoDownloadSelectionChanged(selection: Array<String>) {
|
||||
viewModel.setMobileAutoDownloadValues(selection.toSet())
|
||||
}
|
||||
|
||||
dividerPref()
|
||||
override fun onWifiDataAutoDownloadSelectionChanged(selection: Array<String>) {
|
||||
viewModel.setWifiAutoDownloadValues(selection.toSet())
|
||||
}
|
||||
|
||||
sectionHeaderPref(R.string.DataAndStorageSettingsFragment__media_quality)
|
||||
|
||||
radioListPref(
|
||||
title = DSLSettingsText.from(R.string.DataAndStorageSettingsFragment__sent_media_quality),
|
||||
listItems = sentMediaQualityLabels,
|
||||
selected = SentMediaQuality.entries.indexOf(state.sentMediaQuality),
|
||||
onSelected = { viewModel.setSentMediaQuality(SentMediaQuality.entries[it]) }
|
||||
)
|
||||
|
||||
textPref(
|
||||
summary = DSLSettingsText.from(R.string.DataAndStorageSettingsFragment__sending_high_quality_media_will_use_more_data)
|
||||
)
|
||||
|
||||
dividerPref()
|
||||
|
||||
sectionHeaderPref(R.string.DataAndStorageSettingsFragment__calls)
|
||||
|
||||
radioListPref(
|
||||
title = DSLSettingsText.from(R.string.preferences_data_and_storage__use_less_data_for_calls),
|
||||
listItems = callDataModeLabels,
|
||||
selected = abs(state.callDataMode.code - 2),
|
||||
onSelected = {
|
||||
viewModel.setCallDataMode(CallDataMode.fromCode(abs(it - 2)))
|
||||
}
|
||||
)
|
||||
|
||||
textPref(
|
||||
summary = DSLSettingsText.from(R.string.preference_data_and_storage__using_less_data_may_improve_calls_on_bad_networks)
|
||||
)
|
||||
|
||||
dividerPref()
|
||||
|
||||
sectionHeaderPref(R.string.preferences_proxy)
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.preferences_use_proxy),
|
||||
summary = DSLSettingsText.from(if (state.isProxyEnabled) R.string.preferences_on else R.string.preferences_off),
|
||||
onClick = {
|
||||
Navigation.findNavController(requireView()).safeNavigate(R.id.action_dataAndStorageSettingsFragment_to_editProxyFragment)
|
||||
}
|
||||
)
|
||||
override fun onRoamingDataAutoDownloadSelectionChanged(selection: Array<String>) {
|
||||
viewModel.setRoamingAutoDownloadValues(selection.toSet())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private interface DataAndStorageSettingsCallbacks {
|
||||
fun onNavigationClick() = Unit
|
||||
fun onManageStorageClick() = Unit
|
||||
fun onSentMediaQualitySelected(code: String) = Unit
|
||||
fun onCallDataModeSelected(code: String) = Unit
|
||||
fun onUseProxyClick() = Unit
|
||||
fun onMobileDataAutoDownloadSelectionChanged(selection: Array<String>) = Unit
|
||||
fun onWifiDataAutoDownloadSelectionChanged(selection: Array<String>) = Unit
|
||||
fun onRoamingDataAutoDownloadSelectionChanged(selection: Array<String>) = Unit
|
||||
|
||||
object Empty : DataAndStorageSettingsCallbacks
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DataAndStorageSettingsScreen(
|
||||
state: DataAndStorageSettingsState,
|
||||
callbacks: DataAndStorageSettingsCallbacks
|
||||
) {
|
||||
Scaffolds.Settings(
|
||||
title = stringResource(R.string.preferences__data_and_storage),
|
||||
onNavigationClick = callbacks::onNavigationClick,
|
||||
navigationIcon = ImageVector.vectorResource(R.drawable.symbol_arrow_start_24)
|
||||
) { paddingValues ->
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.then(rememberStatusBarColorNestedScrollModifier())
|
||||
) {
|
||||
item {
|
||||
Rows.TextRow(
|
||||
text = stringResource(R.string.preferences_data_and_storage__manage_storage),
|
||||
label = state.totalStorageUse.bytes.toUnitString(),
|
||||
onClick = callbacks::onManageStorageClick
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Dividers.Default()
|
||||
}
|
||||
|
||||
item {
|
||||
Texts.SectionHeader(stringResource(R.string.preferences_chats__media_auto_download))
|
||||
}
|
||||
|
||||
item {
|
||||
Rows.MultiSelectRow(
|
||||
text = stringResource(R.string.preferences_chats__when_using_mobile_data),
|
||||
labels = stringArrayResource(R.array.pref_media_download_entries),
|
||||
values = stringArrayResource(R.array.pref_media_download_values),
|
||||
selection = state.mobileAutoDownloadValues.toTypedArray(),
|
||||
onSelectionChanged = callbacks::onMobileDataAutoDownloadSelectionChanged
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Rows.MultiSelectRow(
|
||||
text = stringResource(R.string.preferences_chats__when_using_wifi),
|
||||
labels = stringArrayResource(R.array.pref_media_download_entries),
|
||||
values = stringArrayResource(R.array.pref_media_download_values),
|
||||
selection = state.wifiAutoDownloadValues.toTypedArray(),
|
||||
onSelectionChanged = callbacks::onWifiDataAutoDownloadSelectionChanged
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Rows.MultiSelectRow(
|
||||
text = stringResource(R.string.preferences_chats__when_roaming),
|
||||
labels = stringArrayResource(R.array.pref_media_download_entries),
|
||||
values = stringArrayResource(R.array.pref_media_download_values),
|
||||
selection = state.roamingAutoDownloadValues.toTypedArray(),
|
||||
onSelectionChanged = callbacks::onRoamingDataAutoDownloadSelectionChanged
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Dividers.Default()
|
||||
}
|
||||
|
||||
item {
|
||||
Texts.SectionHeader(stringResource(R.string.DataAndStorageSettingsFragment__media_quality))
|
||||
}
|
||||
|
||||
item {
|
||||
val context = LocalContext.current
|
||||
val labels = remember { SentMediaQuality.getLabels(context) }
|
||||
|
||||
Rows.RadioListRow(
|
||||
text = stringResource(R.string.DataAndStorageSettingsFragment__sent_media_quality),
|
||||
labels = labels,
|
||||
values = SentMediaQuality.entries.map { it.code.toString() }.toTypedArray(),
|
||||
selectedValue = state.sentMediaQuality.code.toString(),
|
||||
onSelected = callbacks::onSentMediaQualitySelected
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Rows.TextRow(
|
||||
text = {
|
||||
Text(
|
||||
text = stringResource(R.string.DataAndStorageSettingsFragment__sending_high_quality_media_will_use_more_data),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Dividers.Default()
|
||||
}
|
||||
|
||||
item {
|
||||
Texts.SectionHeader(stringResource(R.string.DataAndStorageSettingsFragment__calls))
|
||||
}
|
||||
|
||||
item {
|
||||
Rows.RadioListRow(
|
||||
text = stringResource(R.string.preferences_data_and_storage__use_less_data_for_calls),
|
||||
labels = stringArrayResource(R.array.pref_data_and_storage_call_data_mode_values),
|
||||
values = CallDataMode.entries.map { it.code.toString() }.toTypedArray(),
|
||||
selectedValue = abs(state.callDataMode.code - 2).toString(),
|
||||
onSelected = callbacks::onCallDataModeSelected
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Rows.TextRow(
|
||||
text = {
|
||||
Text(
|
||||
text = stringResource(R.string.preference_data_and_storage__using_less_data_may_improve_calls_on_bad_networks),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Dividers.Default()
|
||||
}
|
||||
|
||||
item {
|
||||
Texts.SectionHeader(stringResource(R.string.preferences_proxy))
|
||||
}
|
||||
|
||||
item {
|
||||
Rows.TextRow(
|
||||
text = stringResource(R.string.preferences_use_proxy),
|
||||
label = stringResource(if (state.isProxyEnabled) R.string.preferences_on else R.string.preferences_off),
|
||||
onClick = callbacks::onUseProxyClick
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun DataAndStorageSettingsScreenPreview() {
|
||||
Previews.Preview {
|
||||
DataAndStorageSettingsScreen(
|
||||
state = DataAndStorageSettingsState(
|
||||
totalStorageUse = 100_000,
|
||||
mobileAutoDownloadValues = setOf(),
|
||||
wifiAutoDownloadValues = setOf(),
|
||||
roamingAutoDownloadValues = setOf(),
|
||||
callDataMode = CallDataMode.HIGH_ALWAYS,
|
||||
isProxyEnabled = false,
|
||||
sentMediaQuality = SentMediaQuality.STANDARD
|
||||
),
|
||||
callbacks = DataAndStorageSettingsCallbacks.Empty
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.data
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.mms.SentMediaQuality
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.thoughtcrime.securesms.util.livedata.Store
|
||||
import org.thoughtcrime.securesms.webrtc.CallDataMode
|
||||
|
||||
class DataAndStorageSettingsViewModel(
|
||||
@@ -16,9 +17,9 @@ class DataAndStorageSettingsViewModel(
|
||||
private val repository: DataAndStorageSettingsRepository
|
||||
) : ViewModel() {
|
||||
|
||||
private val store = Store(getState())
|
||||
private val store = MutableStateFlow(getState())
|
||||
|
||||
val state: LiveData<DataAndStorageSettingsState> = store.stateLiveData
|
||||
val state: StateFlow<DataAndStorageSettingsState> = store
|
||||
|
||||
fun refresh() {
|
||||
repository.getTotalStorageUse { totalStorageUse ->
|
||||
|
||||
@@ -68,6 +68,7 @@ import org.signal.core.ui.compose.TextFields.TextField
|
||||
import org.signal.core.util.Base64
|
||||
import org.signal.core.util.Hex
|
||||
import org.signal.core.util.getLength
|
||||
import org.thoughtcrime.securesms.MainActivity
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
||||
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
||||
@@ -78,6 +79,7 @@ import org.thoughtcrime.securesms.components.settings.app.internal.backup.Intern
|
||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.jobs.ArchiveAttachmentReconciliationJob
|
||||
import org.thoughtcrime.securesms.jobs.BackupRestoreMediaJob
|
||||
import org.thoughtcrime.securesms.jobs.LocalBackupJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
@@ -153,6 +155,7 @@ class InternalBackupPlaygroundFragment : ComposeFragment() {
|
||||
onCheckRemoteBackupStateClicked = { viewModel.checkRemoteBackupState() },
|
||||
onEnqueueRemoteBackupClicked = { viewModel.triggerBackupJob() },
|
||||
onEnqueueReconciliationClicked = { AppDependencies.jobManager.add(ArchiveAttachmentReconciliationJob(forced = true)) },
|
||||
onEnqueueMediaRestoreClicked = { AppDependencies.jobManager.add(BackupRestoreMediaJob()) },
|
||||
onHaltAllBackupJobsClicked = { viewModel.haltAllJobs() },
|
||||
onValidateBackupClicked = { viewModel.validateBackup() },
|
||||
onSaveEncryptedBackupToDiskClicked = {
|
||||
@@ -190,7 +193,12 @@ class InternalBackupPlaygroundFragment : ComposeFragment() {
|
||||
MaterialAlertDialogBuilder(context)
|
||||
.setTitle("Are you sure?")
|
||||
.setMessage("This will delete all of your chats! Make sure you've finished a backup first, we don't check for you. Only do this on a test device!")
|
||||
.setPositiveButton("Wipe and restore") { _, _ -> viewModel.wipeAllDataAndRestoreFromRemote() }
|
||||
.setPositiveButton("Wipe and restore") { _, _ ->
|
||||
Toast.makeText(this@InternalBackupPlaygroundFragment.requireContext(), "Restoring backup...", Toast.LENGTH_SHORT).show()
|
||||
viewModel.wipeAllDataAndRestoreFromRemote {
|
||||
startActivity(MainActivity.clearTop(this@InternalBackupPlaygroundFragment.requireActivity()))
|
||||
}
|
||||
}
|
||||
.show()
|
||||
},
|
||||
onImportEncryptedBackupFromDiskClicked = {
|
||||
@@ -329,6 +337,7 @@ fun Screen(
|
||||
onCheckRemoteBackupStateClicked: () -> Unit = {},
|
||||
onEnqueueRemoteBackupClicked: () -> Unit = {},
|
||||
onEnqueueReconciliationClicked: () -> Unit = {},
|
||||
onEnqueueMediaRestoreClicked: () -> Unit = {},
|
||||
onWipeDataAndRestoreFromRemoteClicked: () -> Unit = {},
|
||||
onHaltAllBackupJobsClicked: () -> Unit = {},
|
||||
onSavePlaintextCopyOfRemoteBackupClicked: () -> Unit = {},
|
||||
@@ -400,6 +409,12 @@ fun Screen(
|
||||
onClick = onEnqueueReconciliationClicked
|
||||
)
|
||||
|
||||
Rows.TextRow(
|
||||
text = "Enqueue media restore job",
|
||||
label = "Schedules a job that will restore any NEEDS_RESTORE media.",
|
||||
onClick = onEnqueueMediaRestoreClicked
|
||||
)
|
||||
|
||||
Rows.TextRow(
|
||||
text = "Halt all backup jobs",
|
||||
label = "Stops all backup-related jobs to the best of our ability.",
|
||||
|
||||
@@ -24,6 +24,7 @@ import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.signal.core.util.Hex
|
||||
import org.signal.core.util.ThreadUtil
|
||||
import org.signal.core.util.bytes
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.signal.core.util.copyTo
|
||||
@@ -305,10 +306,10 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
fun wipeAllDataAndRestoreFromRemote() {
|
||||
fun wipeAllDataAndRestoreFromRemote(afterDbRestoreCallback: () -> Unit) {
|
||||
SignalExecutors.BOUNDED_IO.execute {
|
||||
SignalStore.backup.restoreWithCellular = false
|
||||
restoreFromRemote()
|
||||
restoreFromRemote(afterDbRestoreCallback)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -352,12 +353,15 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
|
||||
_state.value = _state.value.copy(dialog = DialogState.None)
|
||||
}
|
||||
|
||||
private fun restoreFromRemote() {
|
||||
private fun restoreFromRemote(afterDbRestoreCallback: () -> Unit) {
|
||||
_state.value = _state.value.copy(statusMessage = "Importing from remote...")
|
||||
|
||||
viewModelScope.launch {
|
||||
when (val result = BackupRepository.restoreRemoteBackup()) {
|
||||
RemoteRestoreResult.Success -> _state.value = _state.value.copy(statusMessage = "Import complete!")
|
||||
RemoteRestoreResult.Success -> {
|
||||
_state.value = _state.value.copy(statusMessage = "Import complete!")
|
||||
ThreadUtil.runOnMain { afterDbRestoreCallback() }
|
||||
}
|
||||
RemoteRestoreResult.Canceled,
|
||||
RemoteRestoreResult.Failure,
|
||||
RemoteRestoreResult.PermanentSvrBFailure,
|
||||
|
||||
@@ -17,6 +17,7 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.signal.core.ui.compose.Dividers
|
||||
import org.signal.core.ui.compose.Rows
|
||||
import org.signal.core.ui.compose.Texts
|
||||
import org.signal.core.util.bytes
|
||||
|
||||
@@ -24,49 +25,65 @@ import org.signal.core.util.bytes
|
||||
fun InternalBackupStatsTab(stats: InternalBackupPlaygroundViewModel.StatsState, callbacks: StatsCallbacks) {
|
||||
val scrollState = rememberScrollState()
|
||||
Column(modifier = Modifier.verticalScroll(scrollState)) {
|
||||
Texts.SectionHeader(text = "Local Attachment State")
|
||||
|
||||
if (stats.attachmentStats != null) {
|
||||
Text(text = "Attachment Count: ${stats.attachmentStats.attachmentCount}")
|
||||
Texts.SectionHeader(text = "Local Attachments")
|
||||
|
||||
Text(text = "Transit Download State:")
|
||||
stats.attachmentStats.transferStateCounts.forEach { (state, count) ->
|
||||
if (count > 0) {
|
||||
Text(text = "$state: $count")
|
||||
}
|
||||
}
|
||||
Rows.TextRow(
|
||||
text = "Total attachment rows",
|
||||
label = "${stats.attachmentStats.totalAttachmentRows}"
|
||||
)
|
||||
|
||||
Text(text = "Valid for archive Transit Download State:")
|
||||
stats.attachmentStats.validForArchiveTransferStateCounts.forEach { (state, count) ->
|
||||
if (count > 0) {
|
||||
Text(text = "$state: $count")
|
||||
}
|
||||
}
|
||||
Rows.TextRow(
|
||||
text = "Total unique data files",
|
||||
label = "${stats.attachmentStats.totalUniqueDataFiles}"
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.size(4.dp))
|
||||
Rows.TextRow(
|
||||
text = "Total unique media names",
|
||||
label = "${stats.attachmentStats.totalUniqueMediaNames}"
|
||||
)
|
||||
|
||||
Text(text = "Archive State:")
|
||||
stats.attachmentStats.archiveStateCounts.forEach { (state, count) ->
|
||||
if (count > 0) {
|
||||
Text(text = "$state: $count")
|
||||
}
|
||||
}
|
||||
Rows.TextRow(
|
||||
text = "Total eligible for upload rows",
|
||||
label = "${stats.attachmentStats.totalEligibleForUploadRows}"
|
||||
)
|
||||
|
||||
Rows.TextRow(
|
||||
text = "Total unique media names eligible for upload ⭐",
|
||||
label = "${stats.attachmentStats.totalUniqueMediaNamesEligibleForUpload}"
|
||||
)
|
||||
|
||||
Rows.TextRow(
|
||||
text = "Eligible attachments by status ⭐",
|
||||
label = stats.attachmentStats.archiveStatusMediaNameCounts.entries.joinToString("\n") { (status, count) -> "$status: $count" }
|
||||
)
|
||||
|
||||
Rows.TextRow(
|
||||
text = "Total media names with thumbnails",
|
||||
label = "${stats.attachmentStats.mediaNamesWithThumbnailsCount}"
|
||||
)
|
||||
|
||||
Rows.TextRow(
|
||||
text = "Eligible thumbnails by status ⭐",
|
||||
label = stats.attachmentStats.archiveStatusMediaNameThumbnailCounts.entries.joinToString("\n") { (status, count) -> "$status: $count" }
|
||||
)
|
||||
|
||||
Rows.TextRow(
|
||||
text = "Pending attachment upload bytes ⭐",
|
||||
label = "${stats.attachmentStats.pendingAttachmentUploadBytes} (~${stats.attachmentStats.pendingAttachmentUploadBytes.bytes.toUnitString()})"
|
||||
)
|
||||
|
||||
Rows.TextRow(
|
||||
text = "Uploaded attachment bytes ⭐",
|
||||
label = "${stats.attachmentStats.uploadedAttachmentBytes} (~${stats.attachmentStats.uploadedAttachmentBytes.bytes.toUnitString()})"
|
||||
)
|
||||
|
||||
Rows.TextRow(
|
||||
text = "Uploaded thumbnail bytes (estimated)",
|
||||
label = "${stats.attachmentStats.uploadedThumbnailBytes} (~${stats.attachmentStats.uploadedThumbnailBytes.bytes.toUnitString()})"
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
|
||||
Text(text = "Unique/archived data files: ${stats.attachmentStats.attachmentFileCount}/${stats.attachmentStats.finishedAttachmentFileCount}")
|
||||
Text(text = "Unique/archived verified plaintextHash count: ${stats.attachmentStats.attachmentPlaintextHashAndKeyCount}/${stats.attachmentStats.finishedAttachmentPlaintextHashAndKeyCount}")
|
||||
Text(text = "Unique/expected thumbnail files: ${stats.attachmentStats.thumbnailFileCount}/${stats.attachmentStats.estimatedThumbnailCount}")
|
||||
Text(text = "Local Total: ${stats.attachmentStats.attachmentFileCount + stats.attachmentStats.thumbnailFileCount}")
|
||||
Text(text = "Expected remote total: ${stats.attachmentStats.estimatedThumbnailCount + stats.attachmentStats.finishedAttachmentPlaintextHashAndKeyCount}")
|
||||
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
|
||||
Text(text = "Pending upload: ${stats.attachmentStats.pendingUploadBytes} (~${stats.attachmentStats.pendingUploadBytes.bytes.toUnitString()})")
|
||||
Text(text = "Est uploaded attachments: ${stats.attachmentStats.uploadedAttachmentBytes} (~${stats.attachmentStats.uploadedAttachmentBytes.bytes.toUnitString()})")
|
||||
Text(text = "Est uploaded thumbnails: ${stats.attachmentStats.thumbnailBytes} (~${stats.attachmentStats.thumbnailBytes.bytes.toUnitString()})")
|
||||
val total = stats.attachmentStats.thumbnailBytes + stats.attachmentStats.uploadedAttachmentBytes
|
||||
Text(text = "Est total: $total (~${total.bytes.toUnitString()})")
|
||||
} else {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
@@ -79,28 +96,43 @@ fun InternalBackupStatsTab(stats: InternalBackupPlaygroundViewModel.StatsState,
|
||||
Button(onClick = callbacks::loadRemoteState) {
|
||||
Text(text = "Load remote stats (expensive and long)")
|
||||
}
|
||||
} else if (stats.remoteFailureMsg != null) {
|
||||
Text(text = stats.remoteFailureMsg)
|
||||
} else if (stats.loadingRemoteStats) {
|
||||
CircularProgressIndicator()
|
||||
} else if (stats.remoteState != null) {
|
||||
Rows.TextRow(
|
||||
"Total media items ⭐",
|
||||
label = "${stats.remoteState.mediaCount}"
|
||||
)
|
||||
|
||||
Rows.TextRow(
|
||||
"Total media size ⭐",
|
||||
label = "${stats.remoteState.mediaSize} (~${stats.remoteState.mediaSize.bytes.toUnitString()})"
|
||||
)
|
||||
|
||||
Rows.TextRow(
|
||||
text = "Server estimated used size",
|
||||
label = "${stats.remoteState.usedSpace} (~${stats.remoteState.usedSpace.bytes.toUnitString()})"
|
||||
)
|
||||
}
|
||||
|
||||
Dividers.Default()
|
||||
|
||||
Texts.SectionHeader(text = "Expected vs Actual")
|
||||
|
||||
if (stats.attachmentStats != null && stats.remoteState != null) {
|
||||
Rows.TextRow(
|
||||
text = "Counts ⭐",
|
||||
label = "Local: ${stats.attachmentStats.totalUploadCount}\nRemote: ${stats.remoteState.mediaCount}"
|
||||
)
|
||||
|
||||
Rows.TextRow(
|
||||
text = "Bytes ⭐",
|
||||
label = "Local: ${stats.attachmentStats.totalUploadBytes} (~${stats.attachmentStats.totalUploadBytes.bytes.toUnitString()}, thumbnails are estimated)\nRemote: ${stats.remoteState.mediaSize} (~${stats.remoteState.mediaSize.bytes.toUnitString()})"
|
||||
)
|
||||
} else {
|
||||
if (stats.loadingRemoteStats) {
|
||||
CircularProgressIndicator()
|
||||
} else if (stats.remoteState != null) {
|
||||
Text(text = "Media item count: ${stats.remoteState.mediaCount}")
|
||||
Text(text = "Media items sum size: ${stats.remoteState.mediaSize} (~${stats.remoteState.mediaSize.bytes.toUnitString()})")
|
||||
Text(text = "Server estimated used size: ${stats.remoteState.usedSpace} (~${stats.remoteState.usedSpace.bytes.toUnitString()})")
|
||||
} else if (stats.remoteFailureMsg != null) {
|
||||
Text(text = stats.remoteFailureMsg)
|
||||
}
|
||||
|
||||
Dividers.Default()
|
||||
|
||||
Texts.SectionHeader(text = "Expected vs Actual")
|
||||
|
||||
if (stats.attachmentStats != null && stats.remoteState != null) {
|
||||
val finished = stats.attachmentStats.finishedAttachmentFileCount
|
||||
val thumbnails = stats.attachmentStats.thumbnailFileCount
|
||||
Text(text = "Expected Count/Actual Remote Count: ${finished + thumbnails} / ${stats.remoteState.mediaCount}")
|
||||
} else {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiStrings
|
||||
import org.thoughtcrime.securesms.components.settings.app.notifications.profiles.AddAllowedMembersViewModel.NotificationProfileAndRecipients
|
||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||
import org.thoughtcrime.securesms.compose.rememberStatusBarColorNestedScrollModifier
|
||||
import org.thoughtcrime.securesms.database.RecipientTable
|
||||
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
|
||||
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfileId
|
||||
@@ -166,7 +167,9 @@ private fun AddAllowedMembersContent(
|
||||
modifier = Modifier.padding(contentPadding)
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.weight(1f)
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.then(rememberStatusBarColorNestedScrollModifier())
|
||||
) {
|
||||
item {
|
||||
Text(
|
||||
|
||||
@@ -42,6 +42,7 @@ import org.signal.core.ui.compose.SignalPreview
|
||||
import org.signal.core.ui.compose.Texts
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||
import org.thoughtcrime.securesms.compose.rememberStatusBarColorNestedScrollModifier
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import org.thoughtcrime.securesms.util.viewModel
|
||||
|
||||
@@ -176,7 +177,9 @@ private fun AdvancedPrivacySettingsScreen(
|
||||
navigationIcon = ImageVector.vectorResource(R.drawable.symbol_arrow_start_24)
|
||||
) { paddingValues ->
|
||||
LazyColumn(
|
||||
modifier = Modifier.padding(paddingValues)
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.then(rememberStatusBarColorNestedScrollModifier())
|
||||
) {
|
||||
item {
|
||||
Rows.ToggleRow(
|
||||
|
||||
@@ -27,6 +27,7 @@ import org.thoughtcrime.securesms.jobs.OptimizeMediaJob
|
||||
import org.thoughtcrime.securesms.jobs.RestoreOptimizedMediaJob
|
||||
import org.thoughtcrime.securesms.keyvalue.KeepMessagesDuration
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.util.Environment
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||
|
||||
class ManageStorageSettingsViewModel : ViewModel() {
|
||||
@@ -134,7 +135,7 @@ class ManageStorageSettingsViewModel : ViewModel() {
|
||||
|
||||
private suspend fun getOnDeviceStorageOptimizationState(): OnDeviceStorageOptimizationState {
|
||||
return when {
|
||||
!RemoteConfig.messageBackups || !SignalStore.backup.areBackupsEnabled || !AppDependencies.billingApi.isApiAvailable() -> OnDeviceStorageOptimizationState.FEATURE_NOT_AVAILABLE
|
||||
!RemoteConfig.messageBackups || !SignalStore.backup.areBackupsEnabled || !AppDependencies.billingApi.isApiAvailable() || (!RemoteConfig.internalUser && !Environment.IS_STAGING) -> OnDeviceStorageOptimizationState.FEATURE_NOT_AVAILABLE
|
||||
SignalStore.backup.backupTier != MessageBackupTier.PAID -> OnDeviceStorageOptimizationState.REQUIRES_PAID_TIER
|
||||
SignalStore.backup.optimizeStorage -> OnDeviceStorageOptimizationState.ENABLED
|
||||
else -> OnDeviceStorageOptimizationState.DISABLED
|
||||
|
||||
@@ -23,6 +23,7 @@ import org.signal.core.ui.compose.Scaffolds
|
||||
import org.signal.core.ui.compose.SignalPreview
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||
import org.thoughtcrime.securesms.compose.rememberStatusBarColorNestedScrollModifier
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.jobs.ApkUpdateJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
@@ -90,7 +91,9 @@ private fun AppUpdatesSettingsScreen(
|
||||
) { paddingValues ->
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.padding(paddingValues)
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.then(rememberStatusBarColorNestedScrollModifier())
|
||||
) {
|
||||
if (Build.VERSION.SDK_INT >= 31) {
|
||||
item {
|
||||
|
||||
@@ -98,6 +98,7 @@ class InternalConversationSettingsFragment : ComposeFragment(), InternalConversa
|
||||
borderless = false,
|
||||
videoGif = false,
|
||||
quote = false,
|
||||
quoteTargetContentType = null,
|
||||
caption = null,
|
||||
stickerLocator = null,
|
||||
blurHash = null,
|
||||
@@ -185,7 +186,7 @@ class InternalConversationSettingsFragment : ComposeFragment(), InternalConversa
|
||||
val id = SignalDatabase.messages.insertMessageOutbox(
|
||||
message = OutgoingMessage(threadRecipient = recipient, sentTimeMillis = time, body = "Outgoing: $i"),
|
||||
threadId = targetThread
|
||||
)
|
||||
).messageId
|
||||
SignalDatabase.messages.markAsSent(id, true)
|
||||
} else {
|
||||
SignalDatabase.messages.insertMessageInbox(
|
||||
@@ -215,7 +216,7 @@ class InternalConversationSettingsFragment : ComposeFragment(), InternalConversa
|
||||
val id = SignalDatabase.messages.insertMessageOutbox(
|
||||
message = OutgoingMessage(threadRecipient = recipient, sentTimeMillis = time, body = "Outgoing: $i", attachments = listOf(attachment)),
|
||||
threadId = targetThread
|
||||
)
|
||||
).messageId
|
||||
SignalDatabase.messages.markAsSent(id, true)
|
||||
SignalDatabase.attachments.getAttachmentsForMessage(id).forEach {
|
||||
SignalDatabase.attachments.debugMakeValidForArchive(it.attachmentId)
|
||||
@@ -249,7 +250,7 @@ class InternalConversationSettingsFragment : ComposeFragment(), InternalConversa
|
||||
splitThreadId,
|
||||
false,
|
||||
null
|
||||
)
|
||||
).messageId
|
||||
SignalDatabase.messages.markAsSent(messageId, true)
|
||||
|
||||
SignalDatabase.threads.update(splitThreadId, true)
|
||||
|
||||
@@ -14,6 +14,7 @@ import org.thoughtcrime.securesms.events.GroupCallRaiseHandEvent
|
||||
import org.thoughtcrime.securesms.events.GroupCallReactionEvent
|
||||
import org.thoughtcrime.securesms.events.WebRtcViewModel
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.ringrtc.CameraState
|
||||
import org.thoughtcrime.securesms.service.webrtc.collections.ParticipantCollection
|
||||
@@ -78,7 +79,7 @@ data class CallParticipantsState(
|
||||
} else {
|
||||
listParticipants.addAll(remoteParticipants.listParticipants)
|
||||
}
|
||||
if (foldableState.isFlat) {
|
||||
if (foldableState.isFlat && !SignalStore.internal.newCallingUi) {
|
||||
listParticipants.add(CallParticipant.EMPTY)
|
||||
}
|
||||
listParticipants.reverse()
|
||||
|
||||
@@ -2,9 +2,15 @@ package org.thoughtcrime.securesms.compose
|
||||
|
||||
import android.animation.ValueAnimator
|
||||
import android.app.Activity
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.Velocity
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.google.android.material.animation.ArgbEvaluatorCompat
|
||||
@@ -14,10 +20,14 @@ import kotlin.math.abs
|
||||
|
||||
/**
|
||||
* Controls status-bar color based off a nested scroll
|
||||
*
|
||||
* Recommended to use this with [rememberStatusBarColorNestedScrollModifier] since it'll prevent you from having to wire through
|
||||
* an activity or the connection to subcomponents.
|
||||
*/
|
||||
class StatusBarColorNestedScrollConnection(
|
||||
private val activity: Activity
|
||||
) : NestedScrollConnection {
|
||||
|
||||
private var animator: ValueAnimator? = null
|
||||
|
||||
private val normalColor = ContextCompat.getColor(activity, R.color.signal_colorBackground)
|
||||
@@ -78,3 +88,21 @@ class StatusBarColorNestedScrollConnection(
|
||||
|
||||
private fun Float.isNearZero(): Boolean = abs(this) < 0.001
|
||||
}
|
||||
|
||||
/**
|
||||
* Remembers the nested scroll modifier to ensure the proper status bar coloring behavior.
|
||||
*
|
||||
* This is only required if the screen you are modifying does not utilize edgeToEdge.
|
||||
*/
|
||||
@Composable
|
||||
fun rememberStatusBarColorNestedScrollModifier(): Modifier {
|
||||
val activity = LocalContext.current as? AppCompatActivity
|
||||
|
||||
return remember {
|
||||
if (activity != null) {
|
||||
Modifier.nestedScroll(StatusBarColorNestedScrollConnection(activity))
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -650,7 +650,7 @@ public class Contact implements Parcelable {
|
||||
|
||||
private static Attachment attachmentFromUri(@Nullable Uri uri) {
|
||||
if (uri == null) return null;
|
||||
return new UriAttachment(uri, MediaUtil.IMAGE_JPEG, AttachmentTable.TRANSFER_PROGRESS_DONE, 0, null, false, false, false, false, null, null, null, null, null);
|
||||
return new UriAttachment(uri, MediaUtil.IMAGE_JPEG, AttachmentTable.TRANSFER_PROGRESS_DONE, 0, null, false, false, false, false, null, null, null, null, null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1649,7 +1649,8 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
quote.isOriginalMissing(),
|
||||
quote.getAttachment(),
|
||||
isStoryReaction(current) ? current.getBody() : null,
|
||||
quote.getQuoteType());
|
||||
quote.getQuoteType(),
|
||||
false);
|
||||
|
||||
quoteView.setWallpaperEnabled(hasWallpaper);
|
||||
quoteView.setVisibility(View.VISIBLE);
|
||||
|
||||
@@ -634,6 +634,16 @@ public final class ConversationUpdateItem extends FrameLayout
|
||||
passthroughClickListener.onClick(v);
|
||||
}
|
||||
});
|
||||
} else if (conversationMessage.getMessageRecord().isUnsupported()) {
|
||||
actionButton.setText(R.string.ConversationFragment__update_build);
|
||||
actionButton.setVisibility(VISIBLE);
|
||||
actionButton.setOnClickListener(v -> {
|
||||
if (batchSelected.isEmpty() && eventListener != null) {
|
||||
eventListener.onUpdateSignalClicked();
|
||||
} else {
|
||||
passthroughClickListener.onClick(v);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
actionButton.setVisibility(GONE);
|
||||
actionButton.setOnClickListener(null);
|
||||
|
||||
@@ -594,6 +594,7 @@ class ConversationFragment :
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
binding.toolbar.isBackInvokedCallbackEnabled = false
|
||||
|
||||
binding.root.setApplyRootInsets(!resources.getWindowSizeClass().isSplitPane())
|
||||
binding.root.setUseWindowTypes(!resources.getWindowSizeClass().isSplitPane())
|
||||
|
||||
disposables.bindTo(viewLifecycleOwner)
|
||||
@@ -1651,11 +1652,6 @@ class ConversationFragment :
|
||||
return
|
||||
}
|
||||
|
||||
if (SignalStore.uiHints.hasNotSeenEditMessageBetaAlert()) {
|
||||
Dialogs.showEditMessageBetaDialog(requireContext()) { handleSendEditMessage() }
|
||||
return
|
||||
}
|
||||
|
||||
val editMessage = inputPanel.editMessage
|
||||
if (editMessage == null) {
|
||||
Log.w(TAG, "No edit message found, forcing exit")
|
||||
@@ -1964,13 +1960,6 @@ class ConversationFragment :
|
||||
return
|
||||
}
|
||||
|
||||
if (SignalStore.uiHints.hasNotSeenTextFormattingAlert() && bodyRanges != null && bodyRanges.ranges.isNotEmpty()) {
|
||||
Dialogs.showFormattedTextDialog(requireContext()) {
|
||||
sendMessage(body, mentions, bodyRanges, messageToEdit, quote, scheduledDate, slideDeck, contacts, clearCompose, linkPreviews, preUploadResults, bypassPreSendSafetyNumberCheck, isViewOnce, afterSendComplete)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (inputPanel.isRecordingInLockedMode) {
|
||||
inputPanel.releaseRecordingLockAndSend()
|
||||
return
|
||||
@@ -3039,6 +3028,10 @@ class ConversationFragment :
|
||||
UnverifiedProfileNameBottomSheet.show(parentFragmentManager, forGroup)
|
||||
}
|
||||
|
||||
override fun onUpdateSignalClicked() {
|
||||
PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(requireContext())
|
||||
}
|
||||
|
||||
override fun onJoinGroupCallClicked() {
|
||||
val activity = activity ?: return
|
||||
val recipient = viewModel.recipientSnapshot ?: return
|
||||
|
||||
@@ -505,7 +505,7 @@ class ConversationRepository(
|
||||
}
|
||||
|
||||
if (messageRecord.isViewOnceMessage()) {
|
||||
val attachment = TombstoneAttachment(MediaUtil.VIEW_ONCE, true)
|
||||
val attachment = TombstoneAttachment.forQuote()
|
||||
slideDeck = SlideDeck()
|
||||
slideDeck.addSlide(MediaUtil.getSlideForAttachment(attachment))
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import org.signal.core.util.Stopwatch
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.toInt
|
||||
import org.signal.paging.PagedDataSource
|
||||
import org.thoughtcrime.securesms.backup.v2.ArchiveRestoreProgress
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupRestoreManager
|
||||
import org.thoughtcrime.securesms.conversation.ConversationData
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage
|
||||
@@ -21,7 +22,6 @@ import org.thoughtcrime.securesms.database.model.InMemoryMessageRecord.Universal
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.messagerequests.MessageRequestRepository
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||
@@ -125,7 +125,7 @@ class ConversationDataSource(
|
||||
records = MessageDataFetcher.updateModelsWithData(records, extraData).toMutableList()
|
||||
stopwatch.split("models")
|
||||
|
||||
if (RemoteConfig.messageBackups && SignalStore.backup.restoreState.inProgress) {
|
||||
if (RemoteConfig.messageBackups && ArchiveRestoreProgress.state.activelyRestoring()) {
|
||||
BackupRestoreManager.prioritizeAttachmentsIfNeeded(records)
|
||||
stopwatch.split("restore")
|
||||
}
|
||||
|
||||
@@ -88,7 +88,8 @@ class V2ConversationItemMediaViewHolder<Model : MappingModel<Model>>(
|
||||
quote.isOriginalMissing,
|
||||
quote.attachment,
|
||||
if (conversationMessage.messageRecord.isStoryReaction()) conversationMessage.messageRecord.body else null,
|
||||
quote.quoteType
|
||||
quote.quoteType,
|
||||
false
|
||||
)
|
||||
|
||||
quoteView.setMessageType(
|
||||
|
||||
@@ -76,10 +76,12 @@ import org.thoughtcrime.securesms.MainFragment;
|
||||
import org.thoughtcrime.securesms.MainNavigator;
|
||||
import org.thoughtcrime.securesms.MuteDialog;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.backup.RestoreState;
|
||||
import org.thoughtcrime.securesms.backup.v2.ArchiveRestoreProgress;
|
||||
import org.thoughtcrime.securesms.backup.v2.ArchiveRestoreProgressState;
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.BackupAlert;
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.BackupAlertBottomSheet;
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.BackupAlertDelegate;
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.status.BackupStatusData;
|
||||
import org.thoughtcrime.securesms.badges.models.Badge;
|
||||
import org.thoughtcrime.securesms.badges.self.expired.ExpiredOneTimeBadgeBottomSheetDialogFragment;
|
||||
import org.thoughtcrime.securesms.badges.self.expired.MonthlyDonationCanceledBottomSheetDialogFragment;
|
||||
@@ -94,9 +96,9 @@ import org.thoughtcrime.securesms.banner.banners.OutdatedBuildBanner;
|
||||
import org.thoughtcrime.securesms.banner.banners.ServiceOutageBanner;
|
||||
import org.thoughtcrime.securesms.banner.banners.UnauthorizedBanner;
|
||||
import org.thoughtcrime.securesms.banner.banners.UsernameOutOfSyncBanner;
|
||||
import org.thoughtcrime.securesms.components.compose.DeleteSyncEducationDialog;
|
||||
import org.thoughtcrime.securesms.components.RatingManager;
|
||||
import org.thoughtcrime.securesms.components.SignalProgressDialog;
|
||||
import org.thoughtcrime.securesms.components.compose.DeleteSyncEducationDialog;
|
||||
import org.thoughtcrime.securesms.components.menu.ActionItem;
|
||||
import org.thoughtcrime.securesms.components.menu.SignalBottomActionBar;
|
||||
import org.thoughtcrime.securesms.components.menu.SignalContextMenu;
|
||||
@@ -746,16 +748,20 @@ public class ConversationListFragment extends MainFragment implements Conversati
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActionClick(@NonNull BackupStatusData backupStatusData) {
|
||||
if (backupStatusData instanceof BackupStatusData.NotEnoughFreeSpace) {
|
||||
BackupAlertBottomSheet.create(new BackupAlert.DiskFull(((BackupStatusData.NotEnoughFreeSpace) backupStatusData).getRequiredSpace()))
|
||||
.show(getParentFragmentManager(), null);
|
||||
} else if (backupStatusData instanceof BackupStatusData.RestoringMedia && ((BackupStatusData.RestoringMedia) backupStatusData).getRestoreStatus() == BackupStatusData.RestoreStatus.WAITING_FOR_WIFI) {
|
||||
public void onActionClick(@NonNull ArchiveRestoreProgressState data) {
|
||||
if (data.getRestoreStatus() == ArchiveRestoreProgressState.RestoreStatus.NOT_ENOUGH_DISK_SPACE) {
|
||||
BackupAlertBottomSheet.create(new BackupAlert.DiskFull(data.getRemainingRestoreSize().toUnitString())).show(getParentFragmentManager(), null);
|
||||
} else if (data.getRestoreState() == RestoreState.RESTORING_MEDIA && data.getRestoreStatus() == ArchiveRestoreProgressState.RestoreStatus.WAITING_FOR_WIFI) {
|
||||
new MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.ResumeRestoreCellular_resume_using_cellular_title)
|
||||
.setMessage(R.string.ResumeRestoreCellular_resume_using_cellular_message)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(R.string.BackupStatus__resume, (d, w) -> SignalStore.backup().setRestoreWithCellular(true))
|
||||
.setPositiveButton(R.string.BackupStatus__resume, (d, w) -> {
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
SignalStore.backup().setRestoreWithCellular(true);
|
||||
ArchiveRestoreProgress.forceUpdate();
|
||||
});
|
||||
})
|
||||
.show();
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -24,6 +24,7 @@ import org.signal.core.util.toInt
|
||||
import org.signal.core.util.update
|
||||
import org.signal.core.util.withinTransaction
|
||||
import org.thoughtcrime.securesms.backup.v2.ArchivedMediaObject
|
||||
import org.thoughtcrime.securesms.util.MediaUtil
|
||||
|
||||
/**
|
||||
* When we delete attachments locally, we can't immediately delete them from the archive CDN. This is because there is still a backup that exists that
|
||||
@@ -135,12 +136,19 @@ class BackupMediaSnapshotTable(context: Context, database: SignalDatabase) : Dat
|
||||
mediaObjects
|
||||
.chunked(SqlUtil.MAX_QUERY_ARGS)
|
||||
.forEach { chunk ->
|
||||
// Full attachment
|
||||
writePendingMediaObjectsChunk(
|
||||
chunk.map { MediaEntry(it.mediaId, it.cdn, it.plaintextHash, it.remoteKey, isThumbnail = false) }
|
||||
chunk
|
||||
.filterNot { MediaUtil.isViewOnceType(it.contentType) || MediaUtil.isLongTextType(it.contentType) }
|
||||
.map { MediaEntry(it.mediaId, it.cdn, it.plaintextHash, it.remoteKey, isThumbnail = false) }
|
||||
)
|
||||
|
||||
// Thumbnail
|
||||
writePendingMediaObjectsChunk(
|
||||
chunk.map { MediaEntry(it.thumbnailMediaId, it.cdn, it.plaintextHash, it.remoteKey, isThumbnail = true) }
|
||||
chunk
|
||||
.filterNot { it.quote }
|
||||
.filter { MediaUtil.isImageOrVideoType(it.contentType) }
|
||||
.map { MediaEntry(it.thumbnailMediaId, it.cdn, it.plaintextHash, it.remoteKey, isThumbnail = true) }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -291,6 +299,10 @@ class BackupMediaSnapshotTable(context: Context, database: SignalDatabase) : Dat
|
||||
}
|
||||
|
||||
private fun writePendingMediaObjectsChunk(chunk: List<MediaEntry>) {
|
||||
if (chunk.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
val values = chunk.map {
|
||||
contentValuesOf(
|
||||
MEDIA_ID to it.mediaId,
|
||||
@@ -324,7 +336,9 @@ class BackupMediaSnapshotTable(context: Context, database: SignalDatabase) : Dat
|
||||
val thumbnailMediaId: String,
|
||||
val cdn: Int?,
|
||||
val plaintextHash: ByteArray,
|
||||
val remoteKey: ByteArray
|
||||
val remoteKey: ByteArray,
|
||||
val quote: Boolean,
|
||||
val contentType: String?
|
||||
)
|
||||
|
||||
class CdnMismatchResult(
|
||||
|
||||
@@ -135,7 +135,7 @@ class InAppPaymentTable(context: Context, databaseHelper: SignalDatabase) : Data
|
||||
endOfPeriod: Duration?,
|
||||
inAppPaymentData: InAppPaymentData
|
||||
): InAppPaymentId {
|
||||
val now = System.currentTimeMillis()
|
||||
val now = System.currentTimeMillis().milliseconds
|
||||
|
||||
validateInAppPayment(state, inAppPaymentData)
|
||||
|
||||
@@ -143,8 +143,8 @@ class InAppPaymentTable(context: Context, databaseHelper: SignalDatabase) : Data
|
||||
.values(
|
||||
TYPE to type.code,
|
||||
STATE to state.code,
|
||||
INSERTED_AT to now,
|
||||
UPDATED_AT to now,
|
||||
INSERTED_AT to now.inWholeMilliseconds,
|
||||
UPDATED_AT to now.inWholeSeconds,
|
||||
SUBSCRIBER_ID to subscriberId?.serialize(),
|
||||
END_OF_PERIOD to (endOfPeriod?.inWholeSeconds ?: 0L),
|
||||
DATA to InAppPaymentData.ADAPTER.encode(inAppPaymentData),
|
||||
@@ -410,7 +410,7 @@ class InAppPaymentTable(context: Context, databaseHelper: SignalDatabase) : Data
|
||||
ID to data.id.serialize(),
|
||||
TYPE to data.type.apply { check(this != InAppPaymentType.UNKNOWN) }.code,
|
||||
STATE to data.state.code,
|
||||
INSERTED_AT to data.insertedAt.inWholeSeconds,
|
||||
INSERTED_AT to data.insertedAt.inWholeMilliseconds,
|
||||
UPDATED_AT to data.updatedAt.inWholeSeconds,
|
||||
NOTIFIED to data.notified,
|
||||
SUBSCRIBER_ID to data.subscriberId?.serialize(),
|
||||
@@ -424,7 +424,7 @@ class InAppPaymentTable(context: Context, databaseHelper: SignalDatabase) : Data
|
||||
id = InAppPaymentId(input.requireLong(ID)),
|
||||
type = InAppPaymentType.deserialize(input.requireInt(TYPE)),
|
||||
state = State.deserialize(input.requireInt(STATE)),
|
||||
insertedAt = input.requireLong(INSERTED_AT).seconds,
|
||||
insertedAt = input.requireLong(INSERTED_AT).milliseconds,
|
||||
updatedAt = input.requireLong(UPDATED_AT).seconds,
|
||||
notified = input.requireBoolean(NOTIFIED),
|
||||
subscriberId = input.requireString(SUBSCRIBER_ID)?.let { SubscriberId.deserialize(it) },
|
||||
|
||||
@@ -39,6 +39,7 @@ class MediaTable internal constructor(context: Context?, databaseHelper: SignalD
|
||||
${AttachmentTable.TABLE_NAME}.${AttachmentTable.WIDTH},
|
||||
${AttachmentTable.TABLE_NAME}.${AttachmentTable.HEIGHT},
|
||||
${AttachmentTable.TABLE_NAME}.${AttachmentTable.QUOTE},
|
||||
${AttachmentTable.TABLE_NAME}.${AttachmentTable.QUOTE_TARGET_CONTENT_TYPE},
|
||||
${AttachmentTable.TABLE_NAME}.${AttachmentTable.STICKER_PACK_ID},
|
||||
${AttachmentTable.TABLE_NAME}.${AttachmentTable.STICKER_PACK_KEY},
|
||||
${AttachmentTable.TABLE_NAME}.${AttachmentTable.STICKER_ID},
|
||||
|
||||
@@ -396,7 +396,8 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
'${AttachmentTable.ARCHIVE_CDN}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.ARCHIVE_CDN},
|
||||
'${AttachmentTable.THUMBNAIL_RESTORE_STATE}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.THUMBNAIL_RESTORE_STATE},
|
||||
'${AttachmentTable.ARCHIVE_TRANSFER_STATE}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.ARCHIVE_TRANSFER_STATE},
|
||||
'${AttachmentTable.ATTACHMENT_UUID}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.ATTACHMENT_UUID}
|
||||
'${AttachmentTable.ATTACHMENT_UUID}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.ATTACHMENT_UUID},
|
||||
'${AttachmentTable.QUOTE_TARGET_CONTENT_TYPE}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.QUOTE_TARGET_CONTENT_TYPE}
|
||||
)
|
||||
) AS ${AttachmentTable.ATTACHMENT_JSON_ALIAS}
|
||||
""".toSingleLine()
|
||||
@@ -2548,11 +2549,11 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
val quoteText = cursor.requireString(QUOTE_BODY)
|
||||
val quoteType = cursor.requireInt(QUOTE_TYPE)
|
||||
val quoteMissing = cursor.requireBoolean(QUOTE_MISSING)
|
||||
val quoteAttachments: List<Attachment> = associatedAttachments.filter { it.quote }.toList()
|
||||
val quoteAttachment: Attachment? = associatedAttachments.filter { it.quote }.firstOrNull()
|
||||
val quoteMentions: List<Mention> = parseQuoteMentions(cursor)
|
||||
val quoteBodyRanges: BodyRangeList? = parseQuoteBodyRanges(cursor)
|
||||
val quote: QuoteModel? = if (quoteId != QUOTE_NOT_PRESENT_ID && quoteAuthor > 0 && (!TextUtils.isEmpty(quoteText) || quoteAttachments.isNotEmpty())) {
|
||||
QuoteModel(quoteId, RecipientId.from(quoteAuthor), quoteText ?: "", quoteMissing, quoteAttachments, quoteMentions, QuoteModel.Type.fromCode(quoteType), quoteBodyRanges)
|
||||
val quote: QuoteModel? = if (quoteId != QUOTE_NOT_PRESENT_ID && quoteAuthor > 0 && (!TextUtils.isEmpty(quoteText) || quoteAttachment != null)) {
|
||||
QuoteModel(quoteId, RecipientId.from(quoteAuthor), quoteText ?: "", quoteMissing, quoteAttachment, quoteMentions, QuoteModel.Type.fromCode(quoteType), quoteBodyRanges)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
@@ -2776,7 +2777,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
contentValues.put(QUOTE_BODY_RANGES, quoteBodyRanges.build().encode())
|
||||
}
|
||||
|
||||
quoteAttachments += retrieved.quote.attachments
|
||||
retrieved.quote.attachment?.let { quoteAttachments += it }
|
||||
} else {
|
||||
contentValues.put(QUOTE_ID, 0)
|
||||
contentValues.put(QUOTE_AUTHOR, 0)
|
||||
@@ -2869,7 +2870,8 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
messageId = messageId,
|
||||
threadId = threadId,
|
||||
threadWasNewlyCreated = threadIdResult.newlyCreated,
|
||||
insertedAttachments = insertedAttachments
|
||||
insertedAttachments = insertedAttachments,
|
||||
quoteAttachmentId = quoteAttachments.firstOrNull()?.let { insertedAttachments?.get(it) }
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -2982,7 +2984,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
threadId: Long,
|
||||
forceSms: Boolean = false,
|
||||
insertListener: InsertListener? = null
|
||||
): Long {
|
||||
): InsertResult {
|
||||
return insertMessageOutbox(
|
||||
message = message,
|
||||
threadId = threadId,
|
||||
@@ -2999,7 +3001,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
forceSms: Boolean,
|
||||
defaultReceiptStatus: Int,
|
||||
insertListener: InsertListener?
|
||||
): Long {
|
||||
): InsertResult {
|
||||
var type = MessageTypes.BASE_SENDING_TYPE
|
||||
var hasSpecialType = false
|
||||
|
||||
@@ -3218,7 +3220,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
}
|
||||
|
||||
if (editedMessage == null) {
|
||||
quoteAttachments += message.outgoingQuote.attachments
|
||||
message.outgoingQuote.attachment?.let { quoteAttachments += it }
|
||||
}
|
||||
} else {
|
||||
contentValues.put(QUOTE_ID, 0)
|
||||
@@ -3320,7 +3322,13 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
|
||||
TrimThreadJob.enqueueAsync(threadId)
|
||||
|
||||
return messageId
|
||||
return InsertResult(
|
||||
messageId = messageId,
|
||||
threadId = threadId,
|
||||
threadWasNewlyCreated = false,
|
||||
insertedAttachments = insertedAttachments,
|
||||
quoteAttachmentId = quoteAttachments.firstOrNull()?.let { insertedAttachments?.get(it) }
|
||||
)
|
||||
}
|
||||
|
||||
private fun hasAudioAttachment(attachments: List<Attachment>): Boolean {
|
||||
@@ -5255,7 +5263,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
timetamp = this.requireLong(DATE_SENT)
|
||||
),
|
||||
expirationInfo = null,
|
||||
storyType = StoryType.fromCode(this.requireInt(STORY_TYPE)),
|
||||
storyType = fromCode(this.requireInt(STORY_TYPE)),
|
||||
dateReceived = this.requireLong(DATE_RECEIVED)
|
||||
)
|
||||
}
|
||||
@@ -5406,7 +5414,8 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
val messageId: Long,
|
||||
val threadId: Long,
|
||||
val threadWasNewlyCreated: Boolean,
|
||||
val insertedAttachments: Map<Attachment, AttachmentId>? = null
|
||||
val insertedAttachments: Map<Attachment, AttachmentId>? = null,
|
||||
val quoteAttachmentId: AttachmentId? = null
|
||||
)
|
||||
|
||||
data class MessageReceiptUpdate(
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.database
|
||||
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import androidx.core.content.contentValuesOf
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.signal.core.util.StreamUtil
|
||||
import org.signal.core.util.delete
|
||||
@@ -98,23 +99,37 @@ class StickerTable(
|
||||
fun insertSticker(sticker: IncomingSticker, dataStream: InputStream, notify: Boolean) {
|
||||
val fileInfo: FileInfo = saveStickerImage(dataStream)
|
||||
|
||||
writableDatabase
|
||||
.insertInto(TABLE_NAME)
|
||||
.values(
|
||||
PACK_ID to sticker.packId,
|
||||
PACK_KEY to sticker.packKey,
|
||||
PACK_TITLE to sticker.packTitle,
|
||||
PACK_AUTHOR to sticker.packAuthor,
|
||||
STICKER_ID to sticker.stickerId,
|
||||
EMOJI to sticker.emoji,
|
||||
CONTENT_TYPE to sticker.contentType,
|
||||
COVER to if (sticker.isCover) 1 else 0,
|
||||
INSTALLED to if (sticker.isInstalled) 1 else 0,
|
||||
FILE_PATH to fileInfo.file.absolutePath,
|
||||
FILE_LENGTH to fileInfo.length,
|
||||
FILE_RANDOM to fileInfo.random
|
||||
)
|
||||
.run(SQLiteDatabase.CONFLICT_REPLACE)
|
||||
val values = contentValuesOf(
|
||||
PACK_ID to sticker.packId,
|
||||
PACK_KEY to sticker.packKey,
|
||||
PACK_TITLE to sticker.packTitle,
|
||||
PACK_AUTHOR to sticker.packAuthor,
|
||||
STICKER_ID to sticker.stickerId,
|
||||
EMOJI to sticker.emoji,
|
||||
CONTENT_TYPE to sticker.contentType,
|
||||
COVER to if (sticker.isCover) 1 else 0,
|
||||
INSTALLED to if (sticker.isInstalled) 1 else 0,
|
||||
FILE_PATH to fileInfo.file.absolutePath,
|
||||
FILE_LENGTH to fileInfo.length,
|
||||
FILE_RANDOM to fileInfo.random
|
||||
)
|
||||
|
||||
var updated = false
|
||||
if (sticker.isCover) {
|
||||
// Archive restore inserts cover rows without a sticker id, try to update first on a reduced uniqueness constraint
|
||||
updated = writableDatabase
|
||||
.update(TABLE_NAME)
|
||||
.values(values)
|
||||
.where("$PACK_ID = ? AND $COVER = 1", sticker.packId)
|
||||
.run() > 0
|
||||
}
|
||||
|
||||
if (!updated) {
|
||||
writableDatabase
|
||||
.insertInto(TABLE_NAME)
|
||||
.values(values)
|
||||
.run(SQLiteDatabase.CONFLICT_REPLACE)
|
||||
}
|
||||
|
||||
notifyStickerListeners()
|
||||
|
||||
@@ -454,7 +469,7 @@ class StickerTable(
|
||||
}
|
||||
}
|
||||
|
||||
class StickerPackRecordReader(private val cursor: Cursor) : Closeable {
|
||||
class StickerPackRecordReader(private val cursor: Cursor) : Closeable, Iterable<StickerPackRecord> {
|
||||
|
||||
fun getNext(): StickerPackRecord? {
|
||||
if (!cursor.moveToNext()) {
|
||||
@@ -486,5 +501,19 @@ class StickerTable(
|
||||
override fun close() {
|
||||
cursor.close()
|
||||
}
|
||||
|
||||
override fun iterator(): Iterator<StickerPackRecord> {
|
||||
return ReaderIterator()
|
||||
}
|
||||
|
||||
private inner class ReaderIterator : Iterator<StickerPackRecord> {
|
||||
override fun hasNext(): Boolean {
|
||||
return cursor.count != 0 && !cursor.isLast
|
||||
}
|
||||
|
||||
override fun next(): StickerPackRecord {
|
||||
return getNext() ?: throw NoSuchElementException()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,6 +141,10 @@ import org.thoughtcrime.securesms.database.helpers.migration.V283_ViewOnceRemote
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V284_SetPlaceholderGroupFlag
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V285_AddEpochToCallLinksTable
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V286_FixRemoteKeyEncoding
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V287_FixInvalidArchiveState
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V288_CopyStickerDataHashStartToEnd
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V289_AddQuoteTargetContentTypeColumn
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V290_AddArchiveThumbnailTransferStateColumn
|
||||
import org.thoughtcrime.securesms.database.SQLiteDatabase as SignalSqliteDatabase
|
||||
|
||||
/**
|
||||
@@ -287,48 +291,52 @@ object SignalDatabaseMigrations {
|
||||
283 to V283_ViewOnceRemoteDataCleanup,
|
||||
284 to V284_SetPlaceholderGroupFlag,
|
||||
285 to V285_AddEpochToCallLinksTable,
|
||||
286 to V286_FixRemoteKeyEncoding
|
||||
286 to V286_FixRemoteKeyEncoding,
|
||||
287 to V287_FixInvalidArchiveState,
|
||||
288 to V288_CopyStickerDataHashStartToEnd,
|
||||
289 to V289_AddQuoteTargetContentTypeColumn,
|
||||
290 to V290_AddArchiveThumbnailTransferStateColumn
|
||||
)
|
||||
|
||||
const val DATABASE_VERSION = 286
|
||||
const val DATABASE_VERSION = 290
|
||||
|
||||
@JvmStatic
|
||||
fun migrate(context: Application, db: SignalSqliteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
val initialForeignKeyState = db.areForeignKeyConstraintsEnabled()
|
||||
|
||||
for (migrationData in migrations) {
|
||||
val eligibleMigrations = migrations.filter { (version, _) -> version > oldVersion && version <= newVersion }
|
||||
|
||||
for (migrationData in eligibleMigrations) {
|
||||
val (version, migration) = migrationData
|
||||
|
||||
if (oldVersion < version) {
|
||||
Log.i(TAG, "Running migration for version $version: ${migration.javaClass.simpleName}. Foreign keys: ${migration.enableForeignKeys}")
|
||||
val startTime = System.currentTimeMillis()
|
||||
Log.i(TAG, "Running migration for version $version: ${migration.javaClass.simpleName}. Foreign keys: ${migration.enableForeignKeys}")
|
||||
val startTime = System.currentTimeMillis()
|
||||
|
||||
var ftsException: SQLiteException? = null
|
||||
var ftsException: SQLiteException? = null
|
||||
|
||||
db.setForeignKeyConstraintsEnabled(migration.enableForeignKeys)
|
||||
db.beginTransaction()
|
||||
try {
|
||||
migration.migrate(context, db, oldVersion, newVersion)
|
||||
db.version = version
|
||||
db.setTransactionSuccessful()
|
||||
} catch (e: SQLiteException) {
|
||||
if (e.message?.contains("invalid fts5 file format") == true || e.message?.contains("vtable constructor failed") == true) {
|
||||
ftsException = e
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
} finally {
|
||||
db.endTransaction()
|
||||
db.setForeignKeyConstraintsEnabled(migration.enableForeignKeys)
|
||||
db.beginTransaction()
|
||||
try {
|
||||
migration.migrate(context, db, oldVersion, newVersion)
|
||||
db.version = version
|
||||
db.setTransactionSuccessful()
|
||||
} catch (e: SQLiteException) {
|
||||
if (e.message?.contains("invalid fts5 file format") == true || e.message?.contains("vtable constructor failed") == true) {
|
||||
ftsException = e
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
|
||||
if (ftsException != null) {
|
||||
Log.w(TAG, "Encountered FTS format issue! Attempting to repair.", ftsException)
|
||||
SignalDatabase.messageSearch.fullyResetTables(db)
|
||||
throw ftsException
|
||||
}
|
||||
|
||||
Log.i(TAG, "Successfully completed migration for version $version in ${System.currentTimeMillis() - startTime} ms")
|
||||
} finally {
|
||||
db.endTransaction()
|
||||
}
|
||||
|
||||
if (ftsException != null) {
|
||||
Log.w(TAG, "Encountered FTS format issue! Attempting to repair.", ftsException)
|
||||
SignalDatabase.messageSearch.fullyResetTables(db)
|
||||
throw ftsException
|
||||
}
|
||||
|
||||
Log.i(TAG, "Successfully completed migration for version $version in ${System.currentTimeMillis() - startTime} ms")
|
||||
}
|
||||
|
||||
db.setForeignKeyConstraintsEnabled(initialForeignKeyState)
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.database.helpers.migration
|
||||
|
||||
import android.app.Application
|
||||
import org.thoughtcrime.securesms.database.SQLiteDatabase
|
||||
|
||||
/**
|
||||
* Ensure archive_transfer_state is clear if an attachment is missing a remote_key.
|
||||
*/
|
||||
@Suppress("ClassName")
|
||||
object V287_FixInvalidArchiveState : SignalDatabaseMigration {
|
||||
override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
db.execSQL("UPDATE attachment SET archive_cdn = null, archive_transfer_state = 0 WHERE remote_key IS NULL AND archive_transfer_state = 3")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.database.helpers.migration
|
||||
|
||||
import android.app.Application
|
||||
import org.thoughtcrime.securesms.database.SQLiteDatabase
|
||||
|
||||
/**
|
||||
* Copy data_hash_start to data_hash_end for sticker attachments that have completed transfer.
|
||||
*/
|
||||
@Suppress("ClassName")
|
||||
object V288_CopyStickerDataHashStartToEnd : SignalDatabaseMigration {
|
||||
override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
db.execSQL(
|
||||
"UPDATE attachment SET data_hash_end = data_hash_start WHERE sticker_pack_id IS NOT NULL AND data_hash_start IS NOT NULL AND data_hash_end IS NULL AND transfer_state = 0"
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.database.helpers.migration
|
||||
|
||||
import android.app.Application
|
||||
import org.thoughtcrime.securesms.database.SQLiteDatabase
|
||||
|
||||
/**
|
||||
* Adds the quote_target_content_type column to attachments and migrates existing quote attachments
|
||||
* to populate this field with their current content_type.
|
||||
*/
|
||||
@Suppress("ClassName")
|
||||
object V289_AddQuoteTargetContentTypeColumn : SignalDatabaseMigration {
|
||||
|
||||
override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
db.execSQL("ALTER TABLE attachment ADD COLUMN quote_target_content_type TEXT DEFAULT NULL;")
|
||||
db.execSQL("UPDATE attachment SET quote_target_content_type = content_type WHERE quote != 0;")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.database.helpers.migration
|
||||
|
||||
import android.app.Application
|
||||
import org.thoughtcrime.securesms.database.SQLiteDatabase
|
||||
|
||||
/**
|
||||
* We need to keep track of a transfer state for thumbnails too.
|
||||
*/
|
||||
@Suppress("ClassName")
|
||||
object V290_AddArchiveThumbnailTransferStateColumn : SignalDatabaseMigration {
|
||||
|
||||
override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
db.execSQL("ALTER TABLE attachment ADD COLUMN archive_thumbnail_transfer_state INTEGER DEFAULT 0;")
|
||||
}
|
||||
}
|
||||
@@ -256,4 +256,8 @@ public abstract class DisplayRecord {
|
||||
public boolean isUnblocked() {
|
||||
return MessageTypes.isUnblocked(type);
|
||||
}
|
||||
|
||||
public boolean isUnsupported() {
|
||||
return MessageTypes.isUnsupportedMessageType(type);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -291,6 +291,8 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||
return staticUpdateDescription(context.getString(isGroupV2() ? R.string.MessageRecord_you_blocked_this_group : R.string.MessageRecord_you_blocked_this_person), Glyph.BLOCK);
|
||||
} else if (isUnblocked()) {
|
||||
return staticUpdateDescription(context.getString(isGroupV2() ? R.string.MessageRecord_you_unblocked_this_group : R.string.MessageRecord_you_unblocked_this_person) , Glyph.THREAD);
|
||||
} else if (isUnsupported()) {
|
||||
return staticUpdateDescription(context.getString(R.string.MessageRecord_unsupported_feature, getFromRecipient().getDisplayName(context)), Glyph.ERROR);
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -730,7 +732,7 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||
isProfileChange() || isGroupV1MigrationEvent() || isChatSessionRefresh() || isBadDecryptType() ||
|
||||
isChangeNumber() || isReleaseChannelDonationRequest() || isThreadMergeEventType() || isSmsExportType() || isSessionSwitchoverEventType() ||
|
||||
isPaymentsRequestToActivate() || isPaymentsActivated() || isReportedSpam() || isMessageRequestAccepted() ||
|
||||
isBlocked() || isUnblocked();
|
||||
isBlocked() || isUnblocked() || isUnsupported();
|
||||
}
|
||||
|
||||
public boolean isMediaPending() {
|
||||
|
||||
@@ -35,6 +35,8 @@ import org.thoughtcrime.securesms.database.PendingRetryReceiptCache;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobMigrator;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.FactoryJobPredicate;
|
||||
import org.thoughtcrime.securesms.jobs.AttachmentCompressionJob;
|
||||
import org.thoughtcrime.securesms.jobs.AttachmentUploadJob;
|
||||
import org.thoughtcrime.securesms.jobs.FastJobStorage;
|
||||
import org.thoughtcrime.securesms.jobs.GroupCallUpdateSendJob;
|
||||
import org.thoughtcrime.securesms.jobs.IndividualSendJob;
|
||||
@@ -44,6 +46,7 @@ import org.thoughtcrime.securesms.jobs.PreKeysSyncJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushGroupSendJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushProcessMessageJob;
|
||||
import org.thoughtcrime.securesms.jobs.ReactionSendJob;
|
||||
import org.thoughtcrime.securesms.jobs.SendDeliveryReceiptJob;
|
||||
import org.thoughtcrime.securesms.jobs.TypingSendJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.megaphone.MegaphoneRepository;
|
||||
@@ -200,7 +203,15 @@ public class ApplicationDependencyProvider implements AppDependencies.Provider {
|
||||
.setJobStorage(new FastJobStorage(JobDatabase.getInstance(context)))
|
||||
.setJobMigrator(new JobMigrator(TextSecurePreferences.getJobManagerVersion(context), JobManager.CURRENT_VERSION, JobManagerFactories.getJobMigrations(context)))
|
||||
.addReservedJobRunner(new FactoryJobPredicate(PushProcessMessageJob.KEY, MarkerJob.KEY))
|
||||
.addReservedJobRunner(new FactoryJobPredicate(IndividualSendJob.KEY, PushGroupSendJob.KEY, ReactionSendJob.KEY, TypingSendJob.KEY, GroupCallUpdateSendJob.KEY))
|
||||
.addReservedJobRunner(new FactoryJobPredicate(AttachmentUploadJob.KEY, AttachmentCompressionJob.KEY))
|
||||
.addReservedJobRunner(new FactoryJobPredicate(
|
||||
IndividualSendJob.KEY,
|
||||
PushGroupSendJob.KEY,
|
||||
ReactionSendJob.KEY,
|
||||
TypingSendJob.KEY,
|
||||
GroupCallUpdateSendJob.KEY,
|
||||
SendDeliveryReceiptJob.KEY
|
||||
))
|
||||
.build();
|
||||
return new JobManager(context, config);
|
||||
}
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
package org.thoughtcrime.securesms.glide;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.Uri;
|
||||
import android.util.Pair;
|
||||
|
||||
import com.bumptech.glide.load.data.StreamLocalUriFetcher;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.attachments.AttachmentId;
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.util.BitmapDecodingException;
|
||||
import org.thoughtcrime.securesms.util.BitmapUtil;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
class DecryptableStreamLocalUriFetcher extends StreamLocalUriFetcher {
|
||||
|
||||
private static final String TAG = Log.tag(DecryptableStreamLocalUriFetcher.class);
|
||||
|
||||
private static final int DIMENSION_LIMIT = 12_000;
|
||||
|
||||
private Context context;
|
||||
|
||||
DecryptableStreamLocalUriFetcher(Context context, Uri uri) {
|
||||
super(context.getContentResolver(), uri);
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected InputStream loadResource(Uri uri, ContentResolver contentResolver) throws FileNotFoundException {
|
||||
if (MediaUtil.hasVideoThumbnail(context, uri)) {
|
||||
Bitmap thumbnail = MediaUtil.getVideoThumbnail(context, uri, 1000);
|
||||
|
||||
if (thumbnail != null) {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
thumbnail.compress(Bitmap.CompressFormat.JPEG, 100, baos);
|
||||
ByteArrayInputStream thumbnailStream = new ByteArrayInputStream(baos.toByteArray());
|
||||
thumbnail.recycle();
|
||||
return thumbnailStream;
|
||||
}
|
||||
if (PartAuthority.isAttachmentUri(uri) && MediaUtil.isVideoType(PartAuthority.getAttachmentContentType(context, uri))) {
|
||||
try {
|
||||
AttachmentId attachmentId = PartAuthority.requireAttachmentId(uri);
|
||||
Uri thumbnailUri = PartAuthority.getAttachmentThumbnailUri(attachmentId);
|
||||
InputStream thumbStream = PartAuthority.getAttachmentThumbnailStream(context, thumbnailUri);
|
||||
if (thumbStream != null) {
|
||||
return thumbStream;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.i(TAG, "Failed to fetch thumbnail", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (PartAuthority.isBlobUri(uri) && BlobProvider.isSingleUseMemoryBlob(uri)) {
|
||||
return PartAuthority.getAttachmentThumbnailStream(context, uri);
|
||||
} else if (isSafeSize(PartAuthority.getAttachmentThumbnailStream(context, uri))) {
|
||||
return PartAuthority.getAttachmentThumbnailStream(context, uri);
|
||||
} else {
|
||||
throw new IOException("File dimensions are too large!");
|
||||
}
|
||||
} catch (IOException ioe) {
|
||||
Log.w(TAG, ioe);
|
||||
throw new FileNotFoundException("PartAuthority couldn't load Uri resource.");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isSafeSize(InputStream stream) {
|
||||
try {
|
||||
Pair<Integer, Integer> size = BitmapUtil.getDimensions(stream);
|
||||
return size.first < DIMENSION_LIMIT && size.second < DIMENSION_LIMIT;
|
||||
} catch (BitmapDecodingException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
package org.thoughtcrime.securesms.glide;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.bumptech.glide.load.Options;
|
||||
import com.bumptech.glide.load.model.ModelLoader;
|
||||
import com.bumptech.glide.load.model.ModelLoaderFactory;
|
||||
import com.bumptech.glide.load.model.MultiModelLoaderFactory;
|
||||
|
||||
import org.thoughtcrime.securesms.mms.DecryptableUri;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
public class DecryptableStreamUriLoader implements ModelLoader<DecryptableUri, InputStream> {
|
||||
|
||||
private final Context context;
|
||||
|
||||
private DecryptableStreamUriLoader(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public LoadData<InputStream> buildLoadData(@NonNull DecryptableUri decryptableUri, int width, int height, @NonNull Options options) {
|
||||
return new LoadData<>(decryptableUri, new DecryptableStreamLocalUriFetcher(context, decryptableUri.getUri()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handles(@NonNull DecryptableUri decryptableUri) {
|
||||
return true;
|
||||
}
|
||||
|
||||
public static class Factory implements ModelLoaderFactory<DecryptableUri, InputStream> {
|
||||
|
||||
private final Context context;
|
||||
|
||||
public Factory(Context context) {
|
||||
this.context = context.getApplicationContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull ModelLoader<DecryptableUri, InputStream> build(@NonNull MultiModelLoaderFactory multiFactory) {
|
||||
return new DecryptableStreamUriLoader(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void teardown() {
|
||||
// Do nothing.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,12 +12,11 @@ import com.bumptech.glide.Registry;
|
||||
import com.bumptech.glide.load.model.GlideUrl;
|
||||
import com.bumptech.glide.load.model.UnitModelLoader;
|
||||
import com.bumptech.glide.load.resource.bitmap.BitmapDrawableEncoder;
|
||||
import com.bumptech.glide.load.resource.bitmap.Downsampler;
|
||||
import com.bumptech.glide.load.resource.bitmap.StreamBitmapDecoder;
|
||||
import com.bumptech.glide.load.resource.gif.ByteBufferGifDecoder;
|
||||
import com.bumptech.glide.load.resource.gif.GifDrawable;
|
||||
import com.bumptech.glide.load.resource.gif.StreamGifDecoder;
|
||||
|
||||
import org.signal.glide.common.io.InputStreamFactory;
|
||||
import org.signal.glide.load.resource.apng.decode.APNGDecoder;
|
||||
import org.thoughtcrime.securesms.badges.load.BadgeLoader;
|
||||
import org.thoughtcrime.securesms.badges.load.GiftBadgeModel;
|
||||
@@ -37,9 +36,14 @@ import org.thoughtcrime.securesms.glide.cache.EncryptedBitmapResourceEncoder;
|
||||
import org.thoughtcrime.securesms.glide.cache.EncryptedCacheDecoder;
|
||||
import org.thoughtcrime.securesms.glide.cache.EncryptedCacheEncoder;
|
||||
import org.thoughtcrime.securesms.glide.cache.EncryptedGifDrawableResourceEncoder;
|
||||
import org.thoughtcrime.securesms.glide.cache.InputStreamFactoryBitmapDecoder;
|
||||
import org.thoughtcrime.securesms.glide.cache.StreamApngDecoder;
|
||||
import org.thoughtcrime.securesms.glide.cache.StreamBitmapDecoder;
|
||||
import org.thoughtcrime.securesms.glide.cache.StreamFactoryApngDecoder;
|
||||
import org.thoughtcrime.securesms.glide.cache.StreamFactoryGifDecoder;
|
||||
import org.thoughtcrime.securesms.glide.cache.WebpSanDecoder;
|
||||
import org.thoughtcrime.securesms.mms.DecryptableUri;
|
||||
import org.thoughtcrime.securesms.mms.DecryptableUriStreamLoader;
|
||||
import org.thoughtcrime.securesms.mms.RegisterGlideComponents;
|
||||
import org.thoughtcrime.securesms.mms.SignalGlideModule;
|
||||
import org.thoughtcrime.securesms.stickers.StickerRemoteUri;
|
||||
@@ -68,10 +72,12 @@ public class SignalGlideComponents implements RegisterGlideComponents {
|
||||
|
||||
registry.prepend(InputStream.class, new EncryptedCacheEncoder(secret, glide.getArrayPool()));
|
||||
|
||||
registry.prepend(File.class, Bitmap.class, new EncryptedCacheDecoder<>(secret, new StreamBitmapDecoder(new Downsampler(registry.getImageHeaderParsers(), context.getResources().getDisplayMetrics(), glide.getBitmapPool(), glide.getArrayPool()), glide.getArrayPool())));
|
||||
registry.prepend(File.class, Bitmap.class, new EncryptedCacheDecoder<>(secret, new StreamBitmapDecoder(context, glide, registry)));
|
||||
|
||||
StreamGifDecoder streamGifDecoder = new StreamGifDecoder(registry.getImageHeaderParsers(), new ByteBufferGifDecoder(context, registry.getImageHeaderParsers(), glide.getBitmapPool(), glide.getArrayPool()), glide.getArrayPool());
|
||||
StreamGifDecoder streamGifDecoder = new StreamGifDecoder(registry.getImageHeaderParsers(), new ByteBufferGifDecoder(context, registry.getImageHeaderParsers(), glide.getBitmapPool(), glide.getArrayPool()), glide.getArrayPool());
|
||||
StreamFactoryGifDecoder streamFactoryGifDecoder = new StreamFactoryGifDecoder(streamGifDecoder);
|
||||
registry.prepend(InputStream.class, GifDrawable.class, streamGifDecoder);
|
||||
registry.prepend(InputStreamFactory.class, GifDrawable.class, streamFactoryGifDecoder);
|
||||
registry.prepend(GifDrawable.class, new EncryptedGifDrawableResourceEncoder(secret));
|
||||
registry.prepend(File.class, GifDrawable.class, new EncryptedCacheDecoder<>(secret, streamGifDecoder));
|
||||
|
||||
@@ -79,13 +85,15 @@ public class SignalGlideComponents implements RegisterGlideComponents {
|
||||
registry.prepend(Bitmap.class, new EncryptedBitmapResourceEncoder(secret));
|
||||
registry.prepend(BitmapDrawable.class, new BitmapDrawableEncoder(glide.getBitmapPool(), encryptedBitmapResourceEncoder));
|
||||
|
||||
ByteBufferApngDecoder apngBufferCacheDecoder = new ByteBufferApngDecoder();
|
||||
StreamApngDecoder apngStreamCacheDecoder = new StreamApngDecoder(apngBufferCacheDecoder);
|
||||
ByteBufferApngDecoder byteBufferApngDecoder = new ByteBufferApngDecoder();
|
||||
StreamApngDecoder streamApngDecoder = new StreamApngDecoder(byteBufferApngDecoder);
|
||||
StreamFactoryApngDecoder streamFactoryApngDecoder = new StreamFactoryApngDecoder(byteBufferApngDecoder, glide, registry);
|
||||
|
||||
registry.prepend(InputStream.class, APNGDecoder.class, apngStreamCacheDecoder);
|
||||
registry.prepend(ByteBuffer.class, APNGDecoder.class, apngBufferCacheDecoder);
|
||||
registry.prepend(InputStream.class, APNGDecoder.class, streamApngDecoder);
|
||||
registry.prepend(InputStreamFactory.class, APNGDecoder.class, streamFactoryApngDecoder);
|
||||
registry.prepend(ByteBuffer.class, APNGDecoder.class, byteBufferApngDecoder);
|
||||
registry.prepend(APNGDecoder.class, new EncryptedApngCacheEncoder(secret));
|
||||
registry.prepend(File.class, APNGDecoder.class, new EncryptedCacheDecoder<>(secret, apngStreamCacheDecoder));
|
||||
registry.prepend(File.class, APNGDecoder.class, new EncryptedCacheDecoder<>(secret, streamApngDecoder));
|
||||
registry.register(APNGDecoder.class, Drawable.class, new ApngFrameDrawableTranscoder());
|
||||
|
||||
registry.prepend(BlurHash.class, Bitmap.class, new BlurHashResourceDecoder());
|
||||
@@ -94,7 +102,8 @@ public class SignalGlideComponents implements RegisterGlideComponents {
|
||||
registry.append(StoryTextPostModel.class, StoryTextPostModel.class, UnitModelLoader.Factory.getInstance());
|
||||
registry.append(ConversationShortcutPhoto.class, Bitmap.class, new ConversationShortcutPhoto.Loader.Factory(context));
|
||||
registry.append(ContactPhoto.class, InputStream.class, new ContactPhotoLoader.Factory(context));
|
||||
registry.append(DecryptableUri.class, InputStream.class, new DecryptableStreamUriLoader.Factory(context));
|
||||
registry.append(DecryptableUri.class, InputStreamFactory.class, new DecryptableUriStreamLoader.Factory(context));
|
||||
registry.append(InputStreamFactory.class, Bitmap.class, new InputStreamFactoryBitmapDecoder(context, glide, registry));
|
||||
registry.append(ChunkedImageUrl.class, InputStream.class, new ChunkedImageUrlLoader.Factory());
|
||||
registry.append(StickerRemoteUri.class, InputStream.class, new StickerRemoteUriLoader.Factory());
|
||||
registry.append(BlurHash.class, BlurHash.class, new BlurHashModelLoader.Factory());
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user