mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-08-19 12:49:54 +01:00
Migrate quotes to have a separate quoteTargetContentType.
This commit is contained in:
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -388,6 +388,7 @@ class AttachmentTableTest {
|
||||
stickerLocator = null,
|
||||
gif = false,
|
||||
quote = false,
|
||||
quoteTargetContentType = null,
|
||||
uuid = UUID.randomUUID(),
|
||||
fileName = null
|
||||
)
|
||||
|
||||
+3
-1
@@ -670,7 +670,9 @@ class AttachmentTableTest_deduping {
|
||||
caption = null,
|
||||
stickerLocator = null,
|
||||
blurHash = null,
|
||||
uuid = UUID.randomUUID()
|
||||
uuid = UUID.randomUUID(),
|
||||
quote = false,
|
||||
quoteTargetContentType = null
|
||||
)
|
||||
),
|
||||
quoteAttachment = emptyList()
|
||||
|
||||
@@ -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,
|
||||
|
||||
+2
-1
@@ -705,7 +705,8 @@ class SyncMessageProcessorTest_synchronizeDeleteForMe {
|
||||
archiveCdn = this.archiveCdn,
|
||||
thumbnailRestoreState = this.thumbnailRestoreState,
|
||||
archiveTransferState = this.archiveTransferState,
|
||||
uuid = uuid
|
||||
uuid = uuid,
|
||||
quoteTargetContentType = this.quoteTargetContentType
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -40,6 +40,7 @@ class LocalStickerAttachment : Attachment {
|
||||
height = StickerSlide.HEIGHT,
|
||||
incrementalMacChunkSize = 0,
|
||||
quote = false,
|
||||
quoteTargetContentType = null,
|
||||
uploadTimestamp = 0,
|
||||
caption = null,
|
||||
stickerLocator = stickerLocator,
|
||||
|
||||
@@ -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,
|
||||
|
||||
+3
-3
@@ -1071,7 +1071,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
|
||||
@@ -1158,11 +1158,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
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
+8
-8
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+7
-2
@@ -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)
|
||||
)
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
|
||||
@@ -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(""));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -98,6 +98,7 @@ class InternalConversationSettingsFragment : ComposeFragment(), InternalConversa
|
||||
borderless = false,
|
||||
videoGif = false,
|
||||
quote = false,
|
||||
quoteTargetContentType = null,
|
||||
caption = null,
|
||||
stickerLocator = null,
|
||||
blurHash = null,
|
||||
|
||||
@@ -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);
|
||||
|
||||
+1
-1
@@ -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))
|
||||
}
|
||||
|
||||
+2
-1
@@ -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(
|
||||
|
||||
@@ -180,6 +180,7 @@ class AttachmentTable(
|
||||
const val THUMBNAIL_RESTORE_STATE = "thumbnail_restore_state"
|
||||
const val ATTACHMENT_UUID = "attachment_uuid"
|
||||
const val OFFLOAD_RESTORED_AT = "offload_restored_at"
|
||||
const val QUOTE_TARGET_CONTENT_TYPE = "quote_target_content_type"
|
||||
|
||||
const val ATTACHMENT_JSON_ALIAS = "attachment_json"
|
||||
|
||||
@@ -217,6 +218,7 @@ class AttachmentTable(
|
||||
BORDERLESS,
|
||||
VIDEO_GIF,
|
||||
QUOTE,
|
||||
QUOTE_TARGET_CONTENT_TYPE,
|
||||
WIDTH,
|
||||
HEIGHT,
|
||||
CAPTION,
|
||||
@@ -279,7 +281,8 @@ class AttachmentTable(
|
||||
$THUMBNAIL_RANDOM BLOB DEFAULT NULL,
|
||||
$THUMBNAIL_RESTORE_STATE INTEGER DEFAULT ${ThumbnailRestoreState.NONE.value},
|
||||
$ATTACHMENT_UUID TEXT DEFAULT NULL,
|
||||
$OFFLOAD_RESTORED_AT INTEGER DEFAULT 0
|
||||
$OFFLOAD_RESTORED_AT INTEGER DEFAULT 0,
|
||||
$QUOTE_TARGET_CONTENT_TYPE TEXT DEFAULT NULL
|
||||
)
|
||||
"""
|
||||
|
||||
@@ -423,6 +426,13 @@ class AttachmentTable(
|
||||
.run()
|
||||
}
|
||||
|
||||
fun hasData(attachmentId: AttachmentId): Boolean {
|
||||
return readableDatabase
|
||||
.exists(TABLE_NAME)
|
||||
.where("$ID = ? AND $DATA_FILE NOT NULL", attachmentId)
|
||||
.run()
|
||||
}
|
||||
|
||||
fun getAttachment(attachmentId: AttachmentId): DatabaseAttachment? {
|
||||
return readableDatabase
|
||||
.select(*PROJECTION)
|
||||
@@ -1790,9 +1800,9 @@ class AttachmentTable(
|
||||
for (attachment in attachments) {
|
||||
val attachmentId = when {
|
||||
attachment is LocalStickerAttachment -> insertLocalStickerAttachment(mmsId, attachment)
|
||||
attachment.uri != null -> insertAttachmentWithData(mmsId, attachment, attachment.quote)
|
||||
attachment is ArchivedAttachment -> insertArchivedAttachment(mmsId, attachment, attachment.quote)
|
||||
else -> insertUndownloadedAttachment(mmsId, attachment, attachment.quote)
|
||||
attachment.uri != null -> insertAttachmentWithData(mmsId, attachment)
|
||||
attachment is ArchivedAttachment -> insertArchivedAttachment(mmsId, attachment, quote = false, quoteTargetContentType = null)
|
||||
else -> insertUndownloadedAttachment(mmsId, attachment, quote = false)
|
||||
}
|
||||
|
||||
insertedAttachments[attachment] = attachmentId
|
||||
@@ -1803,8 +1813,8 @@ class AttachmentTable(
|
||||
for (attachment in quoteAttachment) {
|
||||
val attachmentId = when {
|
||||
attachment.uri != null -> insertQuoteAttachment(mmsId, attachment)
|
||||
attachment is ArchivedAttachment -> insertArchivedAttachment(mmsId, attachment, true)
|
||||
else -> insertUndownloadedAttachment(mmsId, attachment, true)
|
||||
attachment is ArchivedAttachment -> insertArchivedAttachment(mmsId, attachment, quote = true, quoteTargetContentType = attachment.quoteTargetContentType)
|
||||
else -> insertUndownloadedAttachment(mmsId, attachment, quote = true)
|
||||
}
|
||||
|
||||
insertedAttachments[attachment] = attachmentId
|
||||
@@ -2040,6 +2050,7 @@ class AttachmentTable(
|
||||
width = jsonObject.getInt(WIDTH),
|
||||
height = jsonObject.getInt(HEIGHT),
|
||||
quote = jsonObject.getInt(QUOTE) != 0,
|
||||
quoteTargetContentType = if (!jsonObject.isNull(QUOTE_TARGET_CONTENT_TYPE)) jsonObject.getString(QUOTE_TARGET_CONTENT_TYPE) else null,
|
||||
caption = jsonObject.getString(CAPTION),
|
||||
stickerLocator = if (jsonObject.getInt(STICKER_ID) >= 0) {
|
||||
StickerLocator(
|
||||
@@ -2394,6 +2405,7 @@ class AttachmentTable(
|
||||
put(WIDTH, attachment.width)
|
||||
put(HEIGHT, attachment.height)
|
||||
put(QUOTE, quote.toInt())
|
||||
put(QUOTE_TARGET_CONTENT_TYPE, attachment.quoteTargetContentType)
|
||||
put(CAPTION, attachment.caption)
|
||||
put(UPLOAD_TIMESTAMP, attachment.uploadTimestamp)
|
||||
put(BLUR_HASH, attachment.blurHash?.hash)
|
||||
@@ -2425,6 +2437,8 @@ class AttachmentTable(
|
||||
/**
|
||||
* When inserting a quote attachment, it looks a lot like a normal attachment insert, but rather than insert the actual data pointed at by the attachment's
|
||||
* URI, we instead want to generate a thumbnail of that attachment and use that instead.
|
||||
*
|
||||
* It's important to note that it's assumed that [attachment] is the attachment that you're *quoting*. We'll use it's contentType as the quoteTargetContentType.
|
||||
*/
|
||||
@Throws(MmsException::class)
|
||||
private fun insertQuoteAttachment(messageId: Long, attachment: Attachment): AttachmentId {
|
||||
@@ -2438,7 +2452,8 @@ class AttachmentTable(
|
||||
messageId = messageId,
|
||||
dataStream = thumbnail.data.inputStream(),
|
||||
attachment = attachment,
|
||||
quote = true
|
||||
quote = true,
|
||||
quoteTargetContentType = attachment.contentType
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2446,7 +2461,7 @@ class AttachmentTable(
|
||||
val attachmentId: AttachmentId = writableDatabase.withinTransaction { db ->
|
||||
val contentValues = ContentValues().apply {
|
||||
put(MESSAGE_ID, messageId)
|
||||
put(CONTENT_TYPE, attachment.contentType)
|
||||
putNull(CONTENT_TYPE)
|
||||
put(VOICE_NOTE, attachment.voiceNote.toInt())
|
||||
put(BORDERLESS, attachment.borderless.toInt())
|
||||
put(VIDEO_GIF, attachment.videoGif.toInt())
|
||||
@@ -2455,6 +2470,7 @@ class AttachmentTable(
|
||||
put(WIDTH, attachment.width)
|
||||
put(HEIGHT, attachment.height)
|
||||
put(QUOTE, 1)
|
||||
put(QUOTE_TARGET_CONTENT_TYPE, attachment.contentType)
|
||||
put(BLUR_HASH, attachment.blurHash?.hash)
|
||||
put(FILE_NAME, attachment.fileName)
|
||||
|
||||
@@ -2529,7 +2545,7 @@ class AttachmentTable(
|
||||
* Callers are expected to later call [finalizeAttachmentAfterDownload] once they have downloaded the data for this attachment.
|
||||
*/
|
||||
@Throws(MmsException::class)
|
||||
private fun insertArchivedAttachment(messageId: Long, attachment: ArchivedAttachment, quote: Boolean): AttachmentId {
|
||||
private fun insertArchivedAttachment(messageId: Long, attachment: ArchivedAttachment, quote: Boolean, quoteTargetContentType: String?): AttachmentId {
|
||||
Log.d(TAG, "[insertArchivedAttachment] Inserting attachment for messageId $messageId.")
|
||||
|
||||
val attachmentId: AttachmentId = writableDatabase.withinTransaction { db ->
|
||||
@@ -2552,6 +2568,7 @@ class AttachmentTable(
|
||||
put(WIDTH, attachment.width)
|
||||
put(HEIGHT, attachment.height)
|
||||
put(QUOTE, quote.toInt())
|
||||
put(QUOTE_TARGET_CONTENT_TYPE, quoteTargetContentType)
|
||||
put(CAPTION, attachment.caption)
|
||||
put(UPLOAD_TIMESTAMP, attachment.uploadTimestamp)
|
||||
put(ARCHIVE_CDN, attachment.archiveCdn)
|
||||
@@ -2681,14 +2698,14 @@ class AttachmentTable(
|
||||
attachmentId = AttachmentId(rowId)
|
||||
}
|
||||
|
||||
return attachmentId
|
||||
return attachmentId as AttachmentId
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts an attachment with existing data. This is likely an outgoing attachment that we're in the process of sending.
|
||||
*/
|
||||
@Throws(MmsException::class)
|
||||
private fun insertAttachmentWithData(messageId: Long, attachment: Attachment, quote: Boolean): AttachmentId {
|
||||
private fun insertAttachmentWithData(messageId: Long, attachment: Attachment): AttachmentId {
|
||||
requireNotNull(attachment.uri) { "Attachment must have a uri!" }
|
||||
|
||||
Log.d(TAG, "[insertAttachmentWithData] Inserting attachment for messageId $messageId. (MessageId: $messageId, ${attachment.uri})")
|
||||
@@ -2699,7 +2716,7 @@ class AttachmentTable(
|
||||
throw MmsException(e)
|
||||
}
|
||||
|
||||
return insertAttachmentWithData(messageId, dataStream, attachment, quote)
|
||||
return insertAttachmentWithData(messageId, dataStream, attachment, quote = false, quoteTargetContentType = null)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2708,7 +2725,7 @@ class AttachmentTable(
|
||||
* @param dataStream The stream to read the data from. This stream will be closed by this method.
|
||||
*/
|
||||
@Throws(MmsException::class)
|
||||
private fun insertAttachmentWithData(messageId: Long, dataStream: InputStream, attachment: Attachment, quote: Boolean): AttachmentId {
|
||||
private fun insertAttachmentWithData(messageId: Long, dataStream: InputStream, attachment: Attachment, quote: Boolean, quoteTargetContentType: String?): AttachmentId {
|
||||
// To avoid performing long-running operations in a transaction, we write the data to an independent file first in a way that doesn't rely on db state.
|
||||
val fileWriteResult: DataFileWriteResult = writeToDataFile(newDataFile(context), dataStream, attachment.transformProperties ?: TransformProperties.empty())
|
||||
Log.d(TAG, "[insertAttachmentWithData] Wrote data to file: ${fileWriteResult.file.absolutePath} (MessageId: $messageId, ${attachment.uri})")
|
||||
@@ -2808,6 +2825,7 @@ class AttachmentTable(
|
||||
contentValues.put(WIDTH, uploadTemplate?.width ?: attachment.width)
|
||||
contentValues.put(HEIGHT, uploadTemplate?.height ?: attachment.height)
|
||||
contentValues.put(QUOTE, quote.toInt())
|
||||
contentValues.put(QUOTE_TARGET_CONTENT_TYPE, quoteTargetContentType)
|
||||
contentValues.put(CAPTION, attachment.caption)
|
||||
contentValues.put(UPLOAD_TIMESTAMP, uploadTemplate?.uploadTimestamp ?: 0)
|
||||
contentValues.put(TRANSFORM_PROPERTIES, transformProperties.serialize())
|
||||
@@ -2849,7 +2867,7 @@ class AttachmentTable(
|
||||
}
|
||||
|
||||
fun insertWallpaper(dataStream: InputStream): AttachmentId {
|
||||
return insertAttachmentWithData(WALLPAPER_MESSAGE_ID, dataStream, WallpaperAttachment(), quote = false).also { id ->
|
||||
return insertAttachmentWithData(WALLPAPER_MESSAGE_ID, dataStream, WallpaperAttachment(), quote = false, quoteTargetContentType = null).also { id ->
|
||||
createRemoteKeyIfNecessary(id)
|
||||
}
|
||||
}
|
||||
@@ -2964,6 +2982,7 @@ class AttachmentTable(
|
||||
width = cursor.requireInt(WIDTH),
|
||||
height = cursor.requireInt(HEIGHT),
|
||||
quote = cursor.requireBoolean(QUOTE),
|
||||
quoteTargetContentType = cursor.requireString(QUOTE_TARGET_CONTENT_TYPE),
|
||||
caption = cursor.requireString(CAPTION),
|
||||
stickerLocator = cursor.readStickerLocator(),
|
||||
blurHash = if (MediaUtil.isAudioType(contentType)) null else BlurHash.parseOrNull(cursor.requireString(BLUR_HASH)),
|
||||
@@ -3148,7 +3167,7 @@ class AttachmentTable(
|
||||
* disk savings.
|
||||
*/
|
||||
@Throws(Exception::class)
|
||||
fun migrationFinalizeQuoteWithData(previousDataFile: String, thumbnail: ImageCompressionUtil.Result): String {
|
||||
fun migrationFinalizeQuoteWithData(previousDataFile: String, thumbnail: ImageCompressionUtil.Result, quoteTargetContentType: String?): String {
|
||||
val newDataFileInfo = writeToDataFile(newDataFile(context), thumbnail.data.inputStream(), TransformProperties.empty())
|
||||
|
||||
writableDatabase
|
||||
@@ -3160,6 +3179,7 @@ class AttachmentTable(
|
||||
DATA_HASH_START to newDataFileInfo.hash,
|
||||
DATA_HASH_END to newDataFileInfo.hash,
|
||||
CONTENT_TYPE to thumbnail.mimeType,
|
||||
QUOTE_TARGET_CONTENT_TYPE to quoteTargetContentType,
|
||||
WIDTH to thumbnail.width,
|
||||
HEIGHT to thumbnail.height,
|
||||
QUOTE to 1
|
||||
|
||||
@@ -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()
|
||||
|
||||
+31
-29
@@ -143,6 +143,7 @@ import org.thoughtcrime.securesms.database.helpers.migration.V285_AddEpochToCall
|
||||
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.SQLiteDatabase as SignalSqliteDatabase
|
||||
|
||||
/**
|
||||
@@ -291,48 +292,49 @@ object SignalDatabaseMigrations {
|
||||
285 to V285_AddEpochToCallLinksTable,
|
||||
286 to V286_FixRemoteKeyEncoding,
|
||||
287 to V287_FixInvalidArchiveState,
|
||||
288 to V288_CopyStickerDataHashStartToEnd
|
||||
288 to V288_CopyStickerDataHashStartToEnd,
|
||||
289 to V289_AddQuoteTargetContentTypeColumn
|
||||
)
|
||||
|
||||
const val DATABASE_VERSION = 288
|
||||
const val DATABASE_VERSION = 289
|
||||
|
||||
@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)
|
||||
|
||||
+22
@@ -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;")
|
||||
}
|
||||
}
|
||||
@@ -374,7 +374,7 @@ public abstract class PushSendJob extends SendJob {
|
||||
Attachment attachment = localQuoteAttachment.get();
|
||||
SignalServiceAttachment quoteAttachmentPointer = getAttachmentPointerFor(localQuoteAttachment.get());
|
||||
|
||||
quoteAttachments.add(new SignalServiceDataMessage.Quote.QuotedAttachment(attachment.videoGif ? MediaUtil.IMAGE_GIF : attachment.contentType,
|
||||
quoteAttachments.add(new SignalServiceDataMessage.Quote.QuotedAttachment(attachment.quoteTargetContentType != null ? attachment.quoteTargetContentType : MediaUtil.IMAGE_JPEG,
|
||||
attachment.fileName,
|
||||
quoteAttachmentPointer));
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ class QuoteThumbnailBackfillJob private constructor(parameters: Parameters) : Jo
|
||||
|
||||
val thumbnail = SignalDatabase.attachments.generateQuoteThumbnail(DecryptableUri(attachment.uri), attachment.contentType, quiet = true)
|
||||
if (thumbnail != null) {
|
||||
SignalDatabase.attachments.migrationFinalizeQuoteWithData(attachment.dataFile, thumbnail)
|
||||
SignalDatabase.attachments.migrationFinalizeQuoteWithData(attachment.dataFile, thumbnail, attachment.contentType)
|
||||
} else {
|
||||
Log.w(TAG, "Failed to generate thumbnail for attachment: ${attachment.id}. Clearing data.")
|
||||
SignalDatabase.attachments.finalizeQuoteWithNoData(attachment.dataFile)
|
||||
|
||||
@@ -488,6 +488,7 @@ public class LinkPreviewRepository {
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null);
|
||||
}
|
||||
|
||||
|
||||
@@ -1110,7 +1110,7 @@ object DataMessageProcessor {
|
||||
.firstOrNull { it.hasData }
|
||||
|
||||
if (quotedMessage.isViewOnce) {
|
||||
thumbnailAttachment = TombstoneAttachment(MediaUtil.VIEW_ONCE, true)
|
||||
thumbnailAttachment = TombstoneAttachment.forQuote()
|
||||
} else if (thumbnailAttachment == null) {
|
||||
thumbnailAttachment = quotedMessage
|
||||
.linkPreviews
|
||||
|
||||
@@ -829,7 +829,7 @@ object SyncMessageProcessor {
|
||||
val giftBadge: GiftBadge? = if (dataMessage.giftBadge?.receiptCredentialPresentation != null) GiftBadge.Builder().redemptionToken(dataMessage.giftBadge!!.receiptCredentialPresentation!!).build() else null
|
||||
val viewOnce: Boolean = dataMessage.isViewOnce == true
|
||||
val bodyRanges: BodyRangeList? = dataMessage.bodyRanges.toBodyRangeList()
|
||||
val syncAttachments: List<Attachment> = listOfNotNull(sticker) + if (viewOnce) listOf<Attachment>(TombstoneAttachment(MediaUtil.VIEW_ONCE, false)) else dataMessage.attachments.toPointersWithinLimit()
|
||||
val syncAttachments: List<Attachment> = listOfNotNull(sticker) + if (viewOnce) listOf<Attachment>(TombstoneAttachment.forNonQuote(MediaUtil.VIEW_ONCE)) else dataMessage.attachments.toPointersWithinLimit()
|
||||
|
||||
val mediaMessage = OutgoingMessage(
|
||||
recipient = recipient,
|
||||
|
||||
@@ -53,6 +53,7 @@ public class AudioSlide extends Slide {
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null));
|
||||
}
|
||||
|
||||
@@ -61,7 +62,7 @@ public class AudioSlide extends Slide {
|
||||
}
|
||||
|
||||
public AudioSlide(Uri uri, long dataSize, String contentType, boolean voiceNote) {
|
||||
super(new UriAttachment(uri, contentType, AttachmentTable.TRANSFER_PROGRESS_STARTED, dataSize, 0, 0, null, null, voiceNote, false, false, false, null, null, null, null, null));
|
||||
super(new UriAttachment(uri, contentType, AttachmentTable.TRANSFER_PROGRESS_STARTED, dataSize, 0, 0, null, null, voiceNote, false, false, false, null, null, null, null, null, null));
|
||||
}
|
||||
|
||||
public AudioSlide(Attachment attachment) {
|
||||
|
||||
@@ -92,6 +92,11 @@ public abstract class Slide {
|
||||
return attachment.fastPreflightId;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getQuoteTargetContentType() {
|
||||
return attachment.quoteTargetContentType;
|
||||
}
|
||||
|
||||
public long getFileSize() {
|
||||
return attachment.size;
|
||||
}
|
||||
@@ -222,6 +227,7 @@ public abstract class Slide {
|
||||
borderless,
|
||||
gif,
|
||||
quote,
|
||||
null,
|
||||
caption,
|
||||
stickerLocator,
|
||||
blurHash,
|
||||
|
||||
@@ -562,7 +562,7 @@ public class MessageSender {
|
||||
{
|
||||
Set<String> finalUploadJobIds = new HashSet<>(uploadJobIds);
|
||||
|
||||
if (quoteAttachmentId != null) {
|
||||
if (quoteAttachmentId != null && SignalDatabase.attachments().hasData(quoteAttachmentId)) {
|
||||
Job uploadJob = new AttachmentUploadJob(quoteAttachmentId);
|
||||
AppDependencies.getJobManager().add(uploadJob);
|
||||
finalUploadJobIds.add(uploadJob.getId());
|
||||
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.database.helpers.migration
|
||||
|
||||
import android.app.Application
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.isEqualTo
|
||||
import assertk.assertions.isNull
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
import org.signal.core.util.SqlUtil
|
||||
import org.signal.core.util.insertInto
|
||||
import org.signal.core.util.select
|
||||
import org.thoughtcrime.securesms.database.AttachmentTable
|
||||
import org.thoughtcrime.securesms.testutil.SignalDatabaseMigrationRule
|
||||
|
||||
@Suppress("ClassName")
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(manifest = Config.NONE, application = Application::class)
|
||||
class V288_AddQuoteTargetContentTypeColumnTest {
|
||||
|
||||
@get:Rule val signalDatabaseRule = SignalDatabaseMigrationRule(287)
|
||||
|
||||
private val db get() = signalDatabaseRule.database
|
||||
|
||||
@Test
|
||||
fun migrate_addsQuoteTargetContentTypeColumn() {
|
||||
V289_AddQuoteTargetContentTypeColumn.migrate(ApplicationProvider.getApplicationContext(), db, 287, 288)
|
||||
|
||||
assertThat(SqlUtil.columnExists(db, AttachmentTable.TABLE_NAME, AttachmentTable.QUOTE_TARGET_CONTENT_TYPE)).isEqualTo(true)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun migrate_whenQuoteAttachmentExists_populatesQuoteTargetContentType() {
|
||||
val quoteAttachmentId = insertAttachment(
|
||||
contentType = "video/mp4",
|
||||
quote = 1
|
||||
)
|
||||
|
||||
V289_AddQuoteTargetContentTypeColumn.migrate(ApplicationProvider.getApplicationContext(), db, 287, 288)
|
||||
|
||||
val cursor = db
|
||||
.select(AttachmentTable.CONTENT_TYPE, AttachmentTable.QUOTE_TARGET_CONTENT_TYPE)
|
||||
.from(AttachmentTable.TABLE_NAME)
|
||||
.where("${AttachmentTable.ID} = ?", quoteAttachmentId.toString())
|
||||
.run()
|
||||
|
||||
cursor.use {
|
||||
assertThat(it.moveToFirst()).isEqualTo(true)
|
||||
assertThat(it.getString(it.getColumnIndexOrThrow(AttachmentTable.CONTENT_TYPE))).isEqualTo("video/mp4")
|
||||
assertThat(it.getString(it.getColumnIndexOrThrow(AttachmentTable.QUOTE_TARGET_CONTENT_TYPE))).isEqualTo("video/mp4")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun migrate_whenNonQuoteAttachmentExists_doesNotPopulateQuoteTargetContentType() {
|
||||
val nonQuoteAttachmentId = insertAttachment(
|
||||
contentType = "image/jpeg",
|
||||
quote = 0
|
||||
)
|
||||
|
||||
V289_AddQuoteTargetContentTypeColumn.migrate(ApplicationProvider.getApplicationContext(), db, 287, 288)
|
||||
|
||||
val cursor = db
|
||||
.select(AttachmentTable.CONTENT_TYPE, AttachmentTable.QUOTE_TARGET_CONTENT_TYPE)
|
||||
.from(AttachmentTable.TABLE_NAME)
|
||||
.where("${AttachmentTable.ID} = ?", nonQuoteAttachmentId.toString())
|
||||
.run()
|
||||
|
||||
cursor.use {
|
||||
assertThat(it.moveToFirst()).isEqualTo(true)
|
||||
assertThat(it.getString(it.getColumnIndexOrThrow(AttachmentTable.CONTENT_TYPE))).isEqualTo("image/jpeg")
|
||||
assertThat(it.getString(it.getColumnIndexOrThrow(AttachmentTable.QUOTE_TARGET_CONTENT_TYPE))).isNull()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun migrate_whenMixedQuoteAndNonQuoteAttachments_onlyPopulatesQuoteAttachments() {
|
||||
val quoteVideoId = insertAttachment(
|
||||
contentType = "video/mp4",
|
||||
quote = 1
|
||||
)
|
||||
val quoteImageId = insertAttachment(
|
||||
contentType = "image/png",
|
||||
quote = 1
|
||||
)
|
||||
val nonQuoteImageId = insertAttachment(
|
||||
contentType = "image/jpeg",
|
||||
quote = 0
|
||||
)
|
||||
val nonQuoteVideoId = insertAttachment(
|
||||
contentType = "video/webm",
|
||||
quote = 0
|
||||
)
|
||||
|
||||
V289_AddQuoteTargetContentTypeColumn.migrate(ApplicationProvider.getApplicationContext(), db, 287, 288)
|
||||
|
||||
assertQuoteTargetContentType(quoteVideoId, expectedContentType = "video/mp4", expectedQuoteTargetContentType = "video/mp4")
|
||||
assertQuoteTargetContentType(quoteImageId, expectedContentType = "image/png", expectedQuoteTargetContentType = "image/png")
|
||||
assertQuoteTargetContentType(nonQuoteImageId, expectedContentType = "image/jpeg", expectedQuoteTargetContentType = null)
|
||||
assertQuoteTargetContentType(nonQuoteVideoId, expectedContentType = "video/webm", expectedQuoteTargetContentType = null)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun migrate_whenQuoteAttachmentWithNullContentType_setsQuoteTargetContentTypeToNull() {
|
||||
val quoteAttachmentId = insertAttachment(
|
||||
contentType = null,
|
||||
quote = 1
|
||||
)
|
||||
|
||||
V289_AddQuoteTargetContentTypeColumn.migrate(ApplicationProvider.getApplicationContext(), db, 287, 288)
|
||||
|
||||
val cursor = db
|
||||
.select(AttachmentTable.CONTENT_TYPE, AttachmentTable.QUOTE_TARGET_CONTENT_TYPE)
|
||||
.from(AttachmentTable.TABLE_NAME)
|
||||
.where("${AttachmentTable.ID} = ?", quoteAttachmentId.toString())
|
||||
.run()
|
||||
|
||||
cursor.use {
|
||||
assertThat(it.moveToFirst()).isEqualTo(true)
|
||||
assertThat(it.getString(it.getColumnIndexOrThrow(AttachmentTable.CONTENT_TYPE))).isNull()
|
||||
assertThat(it.getString(it.getColumnIndexOrThrow(AttachmentTable.QUOTE_TARGET_CONTENT_TYPE))).isNull()
|
||||
}
|
||||
}
|
||||
|
||||
private fun insertAttachment(contentType: String?, quote: Int): Long {
|
||||
return db
|
||||
.insertInto(AttachmentTable.TABLE_NAME)
|
||||
.values(
|
||||
AttachmentTable.MESSAGE_ID to 1L,
|
||||
AttachmentTable.CONTENT_TYPE to contentType,
|
||||
AttachmentTable.DATA_FILE to "/fake/path/attachment.jpg",
|
||||
AttachmentTable.DATA_RANDOM to "/fake/path/attachment.jpg".toByteArray(),
|
||||
AttachmentTable.TRANSFER_STATE to AttachmentTable.TRANSFER_PROGRESS_DONE,
|
||||
AttachmentTable.QUOTE to quote,
|
||||
AttachmentTable.DATA_SIZE to 1024L,
|
||||
AttachmentTable.WIDTH to 100,
|
||||
AttachmentTable.HEIGHT to 100
|
||||
)
|
||||
.run()
|
||||
}
|
||||
|
||||
private fun assertQuoteTargetContentType(attachmentId: Long, expectedContentType: String?, expectedQuoteTargetContentType: String?) {
|
||||
val cursor = db
|
||||
.select(AttachmentTable.CONTENT_TYPE, AttachmentTable.QUOTE_TARGET_CONTENT_TYPE)
|
||||
.from(AttachmentTable.TABLE_NAME)
|
||||
.where("${AttachmentTable.ID} = ?", attachmentId.toString())
|
||||
.run()
|
||||
|
||||
cursor.use {
|
||||
assertThat(it.moveToFirst()).isEqualTo(true)
|
||||
|
||||
val actualContentType = it.getString(it.getColumnIndexOrThrow(AttachmentTable.CONTENT_TYPE))
|
||||
val actualQuoteTargetContentType = it.getString(it.getColumnIndexOrThrow(AttachmentTable.QUOTE_TARGET_CONTENT_TYPE))
|
||||
|
||||
if (expectedContentType == null) {
|
||||
assertThat(actualContentType).isNull()
|
||||
} else {
|
||||
assertThat(actualContentType).isEqualTo(expectedContentType)
|
||||
}
|
||||
|
||||
if (expectedQuoteTargetContentType == null) {
|
||||
assertThat(actualQuoteTargetContentType).isNull()
|
||||
} else {
|
||||
assertThat(actualQuoteTargetContentType).isEqualTo(expectedQuoteTargetContentType)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -255,7 +255,8 @@ class UploadDependencyGraphTest {
|
||||
archiveCdn = 0,
|
||||
thumbnailRestoreState = AttachmentTable.ThumbnailRestoreState.NONE,
|
||||
archiveTransferState = AttachmentTable.ArchiveTransferState.NONE,
|
||||
uuid = null
|
||||
uuid = null,
|
||||
quoteTargetContentType = null
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
@@ -35,6 +36,7 @@ object UriAttachmentBuilder {
|
||||
borderless,
|
||||
videoGif,
|
||||
quote,
|
||||
quoteTargetContentType,
|
||||
caption,
|
||||
stickerLocator,
|
||||
blurHash,
|
||||
|
||||
@@ -100,7 +100,8 @@ object FakeMessageRecords {
|
||||
archiveCdn = archiveCdn,
|
||||
thumbnailRestoreState = thumbnailRestoreState,
|
||||
archiveTransferState = archiveTransferState,
|
||||
uuid = null
|
||||
uuid = null,
|
||||
quoteTargetContentType = null
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user