From 3554f82ea3b140ed236c4f8005d4ccab4357914d Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Wed, 3 Jan 2024 14:43:05 -0500 Subject: [PATCH] Convert AttachmentTable and models to kotlin. --- .../securesms/attachments/Attachment.java | 319 --- .../securesms/attachments/Attachment.kt | 156 ++ .../attachments/AttachmentCreator.kt | 2 - .../attachments/DatabaseAttachment.java | 142 -- .../attachments/DatabaseAttachment.kt | 135 ++ .../MmsNotificationAttachment.java | 45 - .../attachments/PointerAttachment.java | 194 -- .../attachments/PointerAttachment.kt | 192 ++ .../attachments/TombstoneAttachment.java | 36 - .../attachments/TombstoneAttachment.kt | 45 + .../securesms/attachments/UriAttachment.java | 92 - .../securesms/attachments/UriAttachment.kt | 115 ++ .../components/BorderlessImageView.java | 2 +- .../securesms/components/ThumbnailView.java | 6 +- .../conversation/ConversationItem.java | 20 +- .../forward/MultiselectForwardFragmentArgs.kt | 8 +- .../securesms/database/AttachmentTable.java | 1806 ----------------- .../securesms/database/AttachmentTable.kt | 1669 +++++++++++++++ .../securesms/database/MessageTable.kt | 8 +- .../loaders/GroupedThreadMediaLoader.java | 2 +- .../database/model/MmsMessageRecord.java | 4 +- .../jobs/AttachmentCompressionJob.java | 33 +- .../securesms/jobs/AttachmentDownloadJob.java | 42 +- .../securesms/jobs/AttachmentUploadJob.kt | 8 +- .../jobs/LegacyAttachmentUploadJob.java | 46 +- .../securesms/jobs/MmsSendJob.java | 8 +- .../jobs/PushDistributionListSendJob.java | 2 +- .../securesms/jobs/PushSendJob.java | 82 +- .../thoughtcrime/securesms/jobs/SendJob.java | 2 +- .../securesms/linkpreview/LinkPreview.java | 2 +- .../securesms/mediaoverview/MediaActions.java | 2 +- .../mediaoverview/MediaGalleryAllAdapter.java | 12 +- .../MediaOverviewPageFragment.java | 10 +- .../mediapreview/MediaIntentFactory.kt | 2 +- .../mediapreview/MediaPreviewV2Adapter.kt | 4 +- .../mediapreview/MediaPreviewV2Fragment.kt | 2 +- .../mediapreview/MediaPreviewV2ViewModel.kt | 4 +- .../mediasend/MediaUploadRepository.java | 2 +- .../mediasend/v2/MediaSelectionRepository.kt | 2 +- .../migrations/LegacyMigrationJob.java | 12 +- .../securesms/mms/AttachmentManager.java | 4 +- .../thoughtcrime/securesms/mms/GifSlide.java | 2 +- .../securesms/mms/ImageSlide.java | 2 +- .../securesms/mms/MediaConstraints.java | 2 +- .../securesms/mms/PartAuthority.java | 8 +- .../org/thoughtcrime/securesms/mms/Slide.java | 16 +- .../securesms/mms/StickerSlide.java | 6 +- .../securesms/providers/PartProvider.java | 16 +- .../revealable/ViewOnceMessageView.java | 6 +- .../securesms/sharing/MultiShareSender.java | 34 +- .../securesms/sms/MessageSender.java | 8 +- .../securesms/sms/UploadDependencyGraph.kt | 9 +- .../securesms/stickers/StickerLocator.java | 70 - .../securesms/stickers/StickerLocator.kt | 16 + .../thoughtcrime/securesms/stories/Stories.kt | 2 +- .../stories/viewer/post/StoryPostViewModel.kt | 6 +- .../securesms/util/AttachmentUtil.java | 16 +- .../securesms/util/MediaUtil.java | 18 +- .../securesms/video/exo/PartDataSource.java | 17 +- ...chmentDatabaseTransformPropertiesTest.java | 4 +- .../sms/UploadDependencyGraphTest.kt | 58 +- .../org/signal/core/util/CursorExtensions.kt | 17 + 62 files changed, 2626 insertions(+), 2986 deletions(-) delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/attachments/Attachment.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/attachments/Attachment.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachment.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachment.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/attachments/MmsNotificationAttachment.java delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/attachments/PointerAttachment.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/attachments/PointerAttachment.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/attachments/TombstoneAttachment.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/attachments/TombstoneAttachment.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/attachments/UriAttachment.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/attachments/UriAttachment.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/database/AttachmentTable.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/database/AttachmentTable.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/stickers/StickerLocator.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/stickers/StickerLocator.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/Attachment.java b/app/src/main/java/org/thoughtcrime/securesms/attachments/Attachment.java deleted file mode 100644 index 0ee7b7ef18..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/Attachment.java +++ /dev/null @@ -1,319 +0,0 @@ -/* - * Copyright 2023 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.thoughtcrime.securesms.attachments; - -import android.net.Uri; -import android.os.Parcel; -import android.os.Parcelable; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.os.ParcelCompat; - -import org.thoughtcrime.securesms.audio.AudioHash; -import org.thoughtcrime.securesms.blurhash.BlurHash; -import org.thoughtcrime.securesms.database.AttachmentTable; -import org.thoughtcrime.securesms.database.AttachmentTable.TransformProperties; -import org.thoughtcrime.securesms.stickers.StickerLocator; -import org.thoughtcrime.securesms.util.ParcelUtil; - -import java.util.Objects; - -public abstract class Attachment implements Parcelable { - - @NonNull - private final String contentType; - private final int transferState; - private final long size; - - @Nullable - private final String fileName; - - private final int cdnNumber; - - @Nullable - private final String location; - - @Nullable - private final String key; - - @Nullable - private final String relay; - - @Nullable - private final byte[] digest; - - @Nullable - private final byte[] incrementalDigest; - - @Nullable - private final String fastPreflightId; - - private final boolean voiceNote; - private final boolean borderless; - private final boolean videoGif; - private final int width; - private final int height; - private final boolean quote; - private final long uploadTimestamp; - private final int incrementalMacChunkSize; - - @Nullable - private final String caption; - - @Nullable - private final StickerLocator stickerLocator; - - @Nullable - private final BlurHash blurHash; - - @Nullable - private final AudioHash audioHash; - - @NonNull - private final TransformProperties transformProperties; - - public Attachment(@NonNull String contentType, - int transferState, - long size, - @Nullable String fileName, - int cdnNumber, - @Nullable String location, - @Nullable String key, - @Nullable String relay, - @Nullable byte[] digest, - @Nullable byte[] incrementalDigest, - @Nullable String fastPreflightId, - boolean voiceNote, - boolean borderless, - boolean videoGif, - int width, - int height, - int incrementalMacChunkSize, - boolean quote, - long uploadTimestamp, - @Nullable String caption, - @Nullable StickerLocator stickerLocator, - @Nullable BlurHash blurHash, - @Nullable AudioHash audioHash, - @Nullable TransformProperties transformProperties) - { - this.contentType = contentType; - this.transferState = transferState; - this.size = size; - this.fileName = fileName; - this.cdnNumber = cdnNumber; - this.location = location; - this.key = key; - this.relay = relay; - this.digest = digest; - this.incrementalDigest = incrementalDigest; - this.fastPreflightId = fastPreflightId; - this.voiceNote = voiceNote; - this.borderless = borderless; - this.videoGif = videoGif; - this.width = width; - this.height = height; - this.incrementalMacChunkSize = incrementalMacChunkSize; - this.quote = quote; - this.uploadTimestamp = uploadTimestamp; - this.stickerLocator = stickerLocator; - this.caption = caption; - this.blurHash = blurHash; - this.audioHash = audioHash; - this.transformProperties = transformProperties != null ? transformProperties : TransformProperties.empty(); - } - - protected Attachment(Parcel in) { - this.contentType = Objects.requireNonNull(in.readString()); - this.transferState = in.readInt(); - this.size = in.readLong(); - this.fileName = in.readString(); - this.cdnNumber = in.readInt(); - this.location = in.readString(); - this.key = in.readString(); - this.relay = in.readString(); - this.digest = ParcelUtil.readByteArray(in); - this.incrementalDigest = ParcelUtil.readByteArray(in); - this.fastPreflightId = in.readString(); - this.voiceNote = ParcelUtil.readBoolean(in); - this.borderless = ParcelUtil.readBoolean(in); - this.videoGif = ParcelUtil.readBoolean(in); - this.width = in.readInt(); - this.height = in.readInt(); - this.incrementalMacChunkSize = in.readInt(); - this.quote = ParcelUtil.readBoolean(in); - this.uploadTimestamp = in.readLong(); - this.stickerLocator = ParcelCompat.readParcelable(in, StickerLocator.class.getClassLoader(), StickerLocator.class); - this.caption = in.readString(); - this.blurHash = ParcelCompat.readParcelable(in, BlurHash.class.getClassLoader(), BlurHash.class); - this.audioHash = ParcelCompat.readParcelable(in, AudioHash.class.getClassLoader(), AudioHash.class); - this.transformProperties = Objects.requireNonNull(ParcelCompat.readParcelable(in, TransformProperties.class.getClassLoader(), TransformProperties.class)); - } - - @Override - public void writeToParcel(@NonNull Parcel dest, int flags) { - AttachmentCreator.writeSubclass(dest, this); - dest.writeString(contentType); - dest.writeInt(transferState); - dest.writeLong(size); - dest.writeString(fileName); - dest.writeInt(cdnNumber); - dest.writeString(location); - dest.writeString(key); - dest.writeString(relay); - ParcelUtil.writeByteArray(dest, digest); - ParcelUtil.writeByteArray(dest, incrementalDigest); - dest.writeString(fastPreflightId); - ParcelUtil.writeBoolean(dest, voiceNote); - ParcelUtil.writeBoolean(dest, borderless); - ParcelUtil.writeBoolean(dest, videoGif); - dest.writeInt(width); - dest.writeInt(height); - dest.writeInt(incrementalMacChunkSize); - ParcelUtil.writeBoolean(dest, quote); - dest.writeLong(uploadTimestamp); - dest.writeParcelable(stickerLocator, 0); - dest.writeString(caption); - dest.writeParcelable(blurHash, 0); - dest.writeParcelable(audioHash, 0); - dest.writeParcelable(transformProperties, 0); - } - - @Override - public int describeContents() { - return 0; - } - - public static final Creator CREATOR = AttachmentCreator.INSTANCE; - - @Nullable - public abstract Uri getUri(); - - public abstract @Nullable Uri getPublicUri(); - - public int getTransferState() { - return transferState; - } - - public boolean isInProgress() { - return transferState != AttachmentTable.TRANSFER_PROGRESS_DONE && - transferState != AttachmentTable.TRANSFER_PROGRESS_FAILED && - transferState != AttachmentTable.TRANSFER_PROGRESS_PERMANENT_FAILURE; - } - - public boolean isPermanentlyFailed() { - return transferState == AttachmentTable.TRANSFER_PROGRESS_PERMANENT_FAILURE; - } - - public long getSize() { - return size; - } - - @Nullable - public String getFileName() { - return fileName; - } - - @NonNull - public String getContentType() { - return contentType; - } - - public int getCdnNumber() { - return cdnNumber; - } - - @Nullable - public String getLocation() { - return location; - } - - @Nullable - public String getKey() { - return key; - } - - @Nullable - public String getRelay() { - return relay; - } - - @Nullable - public byte[] getDigest() { - return digest; - } - - @Nullable - public byte[] getIncrementalDigest() { - if (incrementalDigest != null && incrementalDigest.length > 0) { - return incrementalDigest; - } else { - return null; - } - } - - @Nullable - public String getFastPreflightId() { - return fastPreflightId; - } - - public boolean isVoiceNote() { - return voiceNote; - } - - public boolean isBorderless() { - return borderless; - } - - public boolean isVideoGif() { - return videoGif; - } - - public int getWidth() { - return width; - } - - public int getHeight() { - return height; - } - - public int getIncrementalMacChunkSize() { - return incrementalMacChunkSize; - } - - public boolean isQuote() { - return quote; - } - - public long getUploadTimestamp() { - return uploadTimestamp; - } - - public boolean isSticker() { - return stickerLocator != null; - } - - public @Nullable StickerLocator getSticker() { - return stickerLocator; - } - - public @Nullable BlurHash getBlurHash() { - return blurHash; - } - - public @Nullable AudioHash getAudioHash() { - return audioHash; - } - - public @Nullable String getCaption() { - return caption; - } - - public @NonNull TransformProperties getTransformProperties() { - return transformProperties; - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/Attachment.kt b/app/src/main/java/org/thoughtcrime/securesms/attachments/Attachment.kt new file mode 100644 index 0000000000..7271ae9b28 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/Attachment.kt @@ -0,0 +1,156 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.thoughtcrime.securesms.attachments + +import android.net.Uri +import android.os.Parcel +import android.os.Parcelable +import androidx.core.os.ParcelCompat +import org.thoughtcrime.securesms.audio.AudioHash +import org.thoughtcrime.securesms.blurhash.BlurHash +import org.thoughtcrime.securesms.database.AttachmentTable +import org.thoughtcrime.securesms.database.AttachmentTable.TransformProperties +import org.thoughtcrime.securesms.stickers.StickerLocator +import org.thoughtcrime.securesms.util.ParcelUtil + +/** + * Note: We have to use our own Parcelable implementation because we need to do custom stuff to preserve + * subclass information. + */ +abstract class Attachment( + @JvmField + val contentType: String, + @JvmField + val transferState: Int, + @JvmField + val size: Long, + @JvmField + val fileName: String?, + @JvmField + val cdnNumber: Int, + @JvmField + val location: String?, + @JvmField + val key: String?, + @JvmField + val relay: String?, + @JvmField + val digest: ByteArray?, + @JvmField + val incrementalDigest: ByteArray?, + @JvmField + val fastPreflightId: String?, + @JvmField + val voiceNote: Boolean, + @JvmField + val borderless: Boolean, + @JvmField + val videoGif: Boolean, + @JvmField + val width: Int, + @JvmField + val height: Int, + @JvmField + val incrementalMacChunkSize: Int, + @JvmField + val quote: Boolean, + @JvmField + val uploadTimestamp: Long, + @JvmField + val caption: String?, + @JvmField + val stickerLocator: StickerLocator?, + @JvmField + val blurHash: BlurHash?, + @JvmField + val audioHash: AudioHash?, + @JvmField + val transformProperties: TransformProperties? +) : Parcelable { + + abstract val uri: Uri? + abstract val publicUri: Uri? + + protected constructor(parcel: Parcel) : this( + contentType = parcel.readString()!!, + transferState = parcel.readInt(), + size = parcel.readLong(), + fileName = parcel.readString(), + cdnNumber = parcel.readInt(), + location = parcel.readString(), + key = parcel.readString(), + relay = parcel.readString(), + digest = ParcelUtil.readByteArray(parcel), + incrementalDigest = ParcelUtil.readByteArray(parcel), + fastPreflightId = parcel.readString(), + voiceNote = ParcelUtil.readBoolean(parcel), + borderless = ParcelUtil.readBoolean(parcel), + videoGif = ParcelUtil.readBoolean(parcel), + width = parcel.readInt(), + height = parcel.readInt(), + incrementalMacChunkSize = parcel.readInt(), + quote = ParcelUtil.readBoolean(parcel), + uploadTimestamp = parcel.readLong(), + caption = parcel.readString(), + stickerLocator = ParcelCompat.readParcelable(parcel, StickerLocator::class.java.classLoader, StickerLocator::class.java), + blurHash = ParcelCompat.readParcelable(parcel, BlurHash::class.java.classLoader, BlurHash::class.java), + audioHash = ParcelCompat.readParcelable(parcel, AudioHash::class.java.classLoader, AudioHash::class.java), + transformProperties = ParcelCompat.readParcelable(parcel, TransformProperties::class.java.classLoader, TransformProperties::class.java) + ) + + override fun writeToParcel(dest: Parcel, flags: Int) { + AttachmentCreator.writeSubclass(dest, this) + dest.writeString(contentType) + dest.writeInt(transferState) + dest.writeLong(size) + dest.writeString(fileName) + dest.writeInt(cdnNumber) + dest.writeString(location) + dest.writeString(key) + dest.writeString(relay) + ParcelUtil.writeByteArray(dest, digest) + ParcelUtil.writeByteArray(dest, incrementalDigest) + dest.writeString(fastPreflightId) + ParcelUtil.writeBoolean(dest, voiceNote) + ParcelUtil.writeBoolean(dest, borderless) + ParcelUtil.writeBoolean(dest, videoGif) + dest.writeInt(width) + dest.writeInt(height) + dest.writeInt(incrementalMacChunkSize) + ParcelUtil.writeBoolean(dest, quote) + dest.writeLong(uploadTimestamp) + dest.writeString(caption) + dest.writeParcelable(stickerLocator, 0) + dest.writeParcelable(blurHash, 0) + dest.writeParcelable(audioHash, 0) + dest.writeParcelable(transformProperties, 0) + } + + override fun describeContents(): Int { + return 0 + } + + val isInProgress: Boolean + get() = transferState != AttachmentTable.TRANSFER_PROGRESS_DONE && transferState != AttachmentTable.TRANSFER_PROGRESS_FAILED && transferState != AttachmentTable.TRANSFER_PROGRESS_PERMANENT_FAILURE + + val isPermanentlyFailed: Boolean + get() = transferState == AttachmentTable.TRANSFER_PROGRESS_PERMANENT_FAILURE + + val isSticker: Boolean + get() = stickerLocator != null + + fun getIncrementalDigest(): ByteArray? { + return if (incrementalDigest != null && incrementalDigest.size > 0) { + incrementalDigest + } else { + null + } + } + + companion object { + @JvmField + val CREATOR: Parcelable.Creator = AttachmentCreator + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/AttachmentCreator.kt b/app/src/main/java/org/thoughtcrime/securesms/attachments/AttachmentCreator.kt index 99b814a2a0..e70b2b61cf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/AttachmentCreator.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/AttachmentCreator.kt @@ -15,7 +15,6 @@ import android.os.Parcelable object AttachmentCreator : Parcelable.Creator { enum class Subclass(val clazz: Class, val code: String) { DATABASE(DatabaseAttachment::class.java, "database"), - MMS_NOTIFICATION(MmsNotificationAttachment::class.java, "mms_notification"), POINTER(PointerAttachment::class.java, "pointer"), TOMBSTONE(TombstoneAttachment::class.java, "tombstone"), URI(UriAttachment::class.java, "uri") @@ -32,7 +31,6 @@ object AttachmentCreator : Parcelable.Creator { return when (Subclass.values().first { rawCode == it.code }) { Subclass.DATABASE -> DatabaseAttachment(source) - Subclass.MMS_NOTIFICATION -> MmsNotificationAttachment(source) Subclass.POINTER -> PointerAttachment(source) Subclass.TOMBSTONE -> TombstoneAttachment(source) Subclass.URI -> UriAttachment(source) diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachment.java b/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachment.java deleted file mode 100644 index c63b92098d..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachment.java +++ /dev/null @@ -1,142 +0,0 @@ -package org.thoughtcrime.securesms.attachments; - -import android.net.Uri; -import android.os.Parcel; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.os.ParcelCompat; - -import org.thoughtcrime.securesms.audio.AudioHash; -import org.thoughtcrime.securesms.blurhash.BlurHash; -import org.thoughtcrime.securesms.database.AttachmentTable.TransformProperties; -import org.thoughtcrime.securesms.mms.PartAuthority; -import org.thoughtcrime.securesms.stickers.StickerLocator; -import org.thoughtcrime.securesms.util.FeatureFlags; -import org.thoughtcrime.securesms.util.ParcelUtil; - -import java.util.Comparator; - -public class DatabaseAttachment extends Attachment { - - private final AttachmentId attachmentId; - private final long mmsId; - private final boolean hasData; - private final boolean hasThumbnail; - private final int displayOrder; - - public DatabaseAttachment(AttachmentId attachmentId, - long mmsId, - boolean hasData, - boolean hasThumbnail, - String contentType, - int transferProgress, - long size, - String fileName, - int cdnNumber, - String location, - String key, - String relay, - byte[] digest, - byte[] incrementalDigest, - int incrementalMacChunkSize, - String fastPreflightId, - boolean voiceNote, - boolean borderless, - boolean videoGif, - int width, - int height, - boolean quote, - @Nullable String caption, - @Nullable StickerLocator stickerLocator, - @Nullable BlurHash blurHash, - @Nullable AudioHash audioHash, - @Nullable TransformProperties transformProperties, - int displayOrder, - long uploadTimestamp) - { - super(contentType, transferProgress, size, fileName, cdnNumber, location, key, relay, digest, incrementalDigest, fastPreflightId, voiceNote, borderless, videoGif, width, height, incrementalMacChunkSize, quote, uploadTimestamp, caption, stickerLocator, blurHash, audioHash, transformProperties); - this.attachmentId = attachmentId; - this.hasData = hasData; - this.hasThumbnail = hasThumbnail; - this.mmsId = mmsId; - this.displayOrder = displayOrder; - } - - protected DatabaseAttachment(Parcel in) { - super(in); - this.attachmentId = ParcelCompat.readParcelable(in, AttachmentId.class.getClassLoader(), AttachmentId.class); - this.hasData = ParcelUtil.readBoolean(in); - this.hasThumbnail = ParcelUtil.readBoolean(in); - this.mmsId = in.readLong(); - this.displayOrder = in.readInt(); - } - - @Override - public void writeToParcel(@NonNull Parcel dest, int flags) { - super.writeToParcel(dest, flags); - dest.writeParcelable(attachmentId, 0); - ParcelUtil.writeBoolean(dest, hasData); - ParcelUtil.writeBoolean(dest, hasThumbnail); - dest.writeLong(mmsId); - dest.writeInt(displayOrder); - } - - @Override - @Nullable - public Uri getUri() { - if (hasData || (FeatureFlags.instantVideoPlayback() && getIncrementalDigest() != null)) { - return PartAuthority.getAttachmentDataUri(attachmentId); - } else { - return null; - } - } - - @Override - public @Nullable Uri getPublicUri() { - if (hasData) { - return PartAuthority.getAttachmentPublicUri(getUri()); - } else { - return null; - } - } - - public AttachmentId getAttachmentId() { - return attachmentId; - } - - public int getDisplayOrder() { - return displayOrder; - } - - @Override - public boolean equals(Object other) { - return other != null && - other instanceof DatabaseAttachment && - ((DatabaseAttachment) other).attachmentId.equals(this.attachmentId); - } - - @Override - public int hashCode() { - return attachmentId.hashCode(); - } - - public long getMmsId() { - return mmsId; - } - - public boolean hasData() { - return hasData; - } - - public boolean hasThumbnail() { - return hasThumbnail; - } - - public static class DisplayOrderComparator implements Comparator { - @Override - public int compare(DatabaseAttachment lhs, DatabaseAttachment rhs) { - return Integer.compare(lhs.getDisplayOrder(), rhs.getDisplayOrder()); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachment.kt b/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachment.kt new file mode 100644 index 0000000000..073a051262 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachment.kt @@ -0,0 +1,135 @@ +package org.thoughtcrime.securesms.attachments + +import android.net.Uri +import android.os.Parcel +import androidx.core.os.ParcelCompat +import org.thoughtcrime.securesms.audio.AudioHash +import org.thoughtcrime.securesms.blurhash.BlurHash +import org.thoughtcrime.securesms.database.AttachmentTable.TransformProperties +import org.thoughtcrime.securesms.mms.PartAuthority +import org.thoughtcrime.securesms.stickers.StickerLocator +import org.thoughtcrime.securesms.util.FeatureFlags +import org.thoughtcrime.securesms.util.ParcelUtil + +class DatabaseAttachment : Attachment { + + @JvmField + val attachmentId: AttachmentId + + @JvmField + val mmsId: Long + + @JvmField + val hasData: Boolean + + private val hasThumbnail: Boolean + val displayOrder: Int + + constructor( + attachmentId: AttachmentId, + mmsId: Long, + hasData: Boolean, + hasThumbnail: Boolean, + contentType: String?, + transferProgress: Int, + size: Long, + fileName: String?, + cdnNumber: Int, + location: String?, + key: String?, + relay: String?, + digest: ByteArray?, + incrementalDigest: ByteArray?, + incrementalMacChunkSize: Int, + fastPreflightId: String?, + voiceNote: Boolean, + borderless: Boolean, + videoGif: Boolean, + width: Int, + height: Int, + quote: Boolean, + caption: String?, + stickerLocator: StickerLocator?, + blurHash: BlurHash?, + audioHash: AudioHash?, + transformProperties: TransformProperties?, + displayOrder: Int, + uploadTimestamp: Long + ) : super( + contentType = contentType!!, + transferState = transferProgress, + size = size, + fileName = fileName, + cdnNumber = cdnNumber, + location = location, + key = key, + relay = relay, + digest = digest, + incrementalDigest = incrementalDigest, + fastPreflightId = fastPreflightId, + voiceNote = voiceNote, + borderless = borderless, + videoGif = videoGif, width = width, + height = height, + incrementalMacChunkSize = incrementalMacChunkSize, + quote = quote, + uploadTimestamp = uploadTimestamp, + caption = caption, + stickerLocator = stickerLocator, + blurHash = blurHash, + audioHash = audioHash, + transformProperties = transformProperties + ) { + this.attachmentId = attachmentId + this.mmsId = mmsId + this.hasData = hasData + this.hasThumbnail = hasThumbnail + this.displayOrder = displayOrder + } + + constructor(parcel: Parcel) : super(parcel) { + attachmentId = ParcelCompat.readParcelable(parcel, AttachmentId::class.java.classLoader, AttachmentId::class.java)!! + hasData = ParcelUtil.readBoolean(parcel) + hasThumbnail = ParcelUtil.readBoolean(parcel) + mmsId = parcel.readLong() + displayOrder = parcel.readInt() + } + + override fun writeToParcel(dest: Parcel, flags: Int) { + super.writeToParcel(dest, flags) + dest.writeParcelable(attachmentId, 0) + ParcelUtil.writeBoolean(dest, hasData) + ParcelUtil.writeBoolean(dest, hasThumbnail) + dest.writeLong(mmsId) + dest.writeInt(displayOrder) + } + + override val uri: Uri? + get() = if (hasData || FeatureFlags.instantVideoPlayback() && getIncrementalDigest() != null) { + PartAuthority.getAttachmentDataUri(attachmentId) + } else { + null + } + + override val publicUri: Uri? + get() = if (hasData) { + PartAuthority.getAttachmentPublicUri(uri) + } else { + null + } + + override fun equals(other: Any?): Boolean { + return other != null && + other is DatabaseAttachment && other.attachmentId == attachmentId + } + + override fun hashCode(): Int { + return attachmentId.hashCode() + } + + class DisplayOrderComparator : Comparator { + override fun compare(lhs: DatabaseAttachment, rhs: DatabaseAttachment): Int { + return lhs.displayOrder.compareTo(rhs.displayOrder) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/MmsNotificationAttachment.java b/app/src/main/java/org/thoughtcrime/securesms/attachments/MmsNotificationAttachment.java deleted file mode 100644 index 9bc99d5c40..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/MmsNotificationAttachment.java +++ /dev/null @@ -1,45 +0,0 @@ -package org.thoughtcrime.securesms.attachments; - - -import android.net.Uri; -import android.os.Parcel; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.thoughtcrime.securesms.database.AttachmentTable; -import org.thoughtcrime.securesms.database.MessageTable; - -public class MmsNotificationAttachment extends Attachment { - - public MmsNotificationAttachment(int status, long size) { - super("application/mms", getTransferStateFromStatus(status), size, null, 0, null, null, null, null, null, null, false, false, false, 0, 0, 0, false, 0, null, null, null, null, null); - } - - protected MmsNotificationAttachment(Parcel in) { - super(in); - } - - @Nullable - @Override - public Uri getUri() { - return null; - } - - @Override - public @Nullable Uri getPublicUri() { - return null; - } - - private static int getTransferStateFromStatus(int status) { - if (status == MessageTable.MmsStatus.DOWNLOAD_INITIALIZED || - status == MessageTable.MmsStatus.DOWNLOAD_NO_CONNECTIVITY) - { - return AttachmentTable.TRANSFER_PROGRESS_PENDING; - } else if (status == MessageTable.MmsStatus.DOWNLOAD_CONNECTING) { - return AttachmentTable.TRANSFER_PROGRESS_STARTED; - } else { - return AttachmentTable.TRANSFER_PROGRESS_FAILED; - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/PointerAttachment.java b/app/src/main/java/org/thoughtcrime/securesms/attachments/PointerAttachment.java deleted file mode 100644 index a4265eed34..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/PointerAttachment.java +++ /dev/null @@ -1,194 +0,0 @@ -package org.thoughtcrime.securesms.attachments; - -import android.net.Uri; -import android.os.Parcel; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.thoughtcrime.securesms.blurhash.BlurHash; -import org.thoughtcrime.securesms.database.AttachmentTable; -import org.thoughtcrime.securesms.stickers.StickerLocator; -import org.signal.core.util.Base64; -import org.whispersystems.signalservice.api.InvalidMessageStructureException; -import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; -import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; -import org.whispersystems.signalservice.api.util.AttachmentPointerUtil; -import org.whispersystems.signalservice.internal.push.DataMessage; - -import java.util.LinkedList; -import java.util.List; -import java.util.Optional; - -public class PointerAttachment extends Attachment { - - private PointerAttachment(@NonNull String contentType, - int transferState, - long size, - @Nullable String fileName, - int cdnNumber, - @NonNull String location, - @Nullable String key, - @Nullable String relay, - @Nullable byte[] digest, - @Nullable byte[] incrementalDigest, - int incrementalMacChunkSize, - @Nullable String fastPreflightId, - boolean voiceNote, - boolean borderless, - boolean videoGif, - int width, - int height, - long uploadTimestamp, - @Nullable String caption, - @Nullable StickerLocator stickerLocator, - @Nullable BlurHash blurHash) - { - super(contentType, transferState, size, fileName, cdnNumber, location, key, relay, digest, incrementalDigest, fastPreflightId, voiceNote, borderless, videoGif, width, height, incrementalMacChunkSize, false, uploadTimestamp, caption, stickerLocator, blurHash, null, null); - } - - protected PointerAttachment(Parcel in) { - super(in); - } - - @Nullable - @Override - public Uri getUri() { - return null; - } - - @Override - public @Nullable Uri getPublicUri() { - return null; - } - - public static List forPointers(Optional> pointers) { - List results = new LinkedList<>(); - - if (pointers.isPresent()) { - for (SignalServiceAttachment pointer : pointers.get()) { - Optional result = forPointer(Optional.of(pointer)); - - if (result.isPresent()) { - results.add(result.get()); - } - } - } - - return results; - } - - public static List forPointers(@Nullable List pointers) { - List results = new LinkedList<>(); - - if (pointers != null) { - for (SignalServiceDataMessage.Quote.QuotedAttachment pointer : pointers) { - Optional result = forPointer(pointer); - - if (result.isPresent()) { - results.add(result.get()); - } - } - } - - return results; - } - - public static Optional forPointer(Optional pointer) { - return forPointer(pointer, null, null); - } - - public static Optional forPointer(Optional pointer, @Nullable StickerLocator stickerLocator) { - return forPointer(pointer, stickerLocator, null); - } - - public static Optional forPointer(Optional pointer, @Nullable StickerLocator stickerLocator, @Nullable String fastPreflightId) { - if (!pointer.isPresent() || !pointer.get().isPointer()) return Optional.empty(); - - String encodedKey = null; - - if (pointer.get().asPointer().getKey() != null) { - encodedKey = Base64.encodeWithPadding(pointer.get().asPointer().getKey()); - } - - return Optional.of(new PointerAttachment(pointer.get().getContentType(), - AttachmentTable.TRANSFER_PROGRESS_PENDING, - pointer.get().asPointer().getSize().orElse(0), - pointer.get().asPointer().getFileName().orElse(null), - pointer.get().asPointer().getCdnNumber(), - pointer.get().asPointer().getRemoteId().toString(), - encodedKey, - null, - pointer.get().asPointer().getDigest().orElse(null), - pointer.get().asPointer().getIncrementalDigest().orElse(null), - pointer.get().asPointer().getIncrementalMacChunkSize(), - fastPreflightId, - pointer.get().asPointer().getVoiceNote(), - pointer.get().asPointer().isBorderless(), - pointer.get().asPointer().isGif(), - pointer.get().asPointer().getWidth(), - pointer.get().asPointer().getHeight(), - pointer.get().asPointer().getUploadTimestamp(), - pointer.get().asPointer().getCaption().orElse(null), - stickerLocator, - BlurHash.parseOrNull(pointer.get().asPointer().getBlurHash().orElse(null)))); - - } - - public static Optional forPointer(SignalServiceDataMessage.Quote.QuotedAttachment pointer) { - SignalServiceAttachment thumbnail = pointer.getThumbnail(); - - return Optional.of(new PointerAttachment(pointer.getContentType(), - AttachmentTable.TRANSFER_PROGRESS_PENDING, - thumbnail != null ? thumbnail.asPointer().getSize().orElse(0) : 0, - pointer.getFileName(), - thumbnail != null ? thumbnail.asPointer().getCdnNumber() : 0, - thumbnail != null ? thumbnail.asPointer().getRemoteId().toString() : "0", - thumbnail != null && thumbnail.asPointer().getKey() != null ? Base64.encodeWithPadding(thumbnail.asPointer().getKey()) : null, - null, - thumbnail != null ? thumbnail.asPointer().getDigest().orElse(null) : null, - thumbnail != null ? thumbnail.asPointer().getIncrementalDigest().orElse(null) : null, - thumbnail != null ? thumbnail.asPointer().getIncrementalMacChunkSize() : 0, - null, - false, - false, - false, - thumbnail != null ? thumbnail.asPointer().getWidth() : 0, - thumbnail != null ? thumbnail.asPointer().getHeight() : 0, - thumbnail != null ? thumbnail.asPointer().getUploadTimestamp() : 0, - thumbnail != null ? thumbnail.asPointer().getCaption().orElse(null) : null, - null, - null)); - } - - public static Optional forPointer(DataMessage.Quote.QuotedAttachment quotedAttachment) { - SignalServiceAttachment thumbnail; - try { - thumbnail = quotedAttachment.thumbnail != null ? AttachmentPointerUtil.createSignalAttachmentPointer(quotedAttachment.thumbnail) : null; - } catch (InvalidMessageStructureException e) { - return Optional.empty(); - } - - return Optional.of(new PointerAttachment(quotedAttachment.contentType, - AttachmentTable.TRANSFER_PROGRESS_PENDING, - thumbnail != null ? thumbnail.asPointer().getSize().orElse(0) : 0, - quotedAttachment.fileName, - thumbnail != null ? thumbnail.asPointer().getCdnNumber() : 0, - thumbnail != null ? thumbnail.asPointer().getRemoteId().toString() : "0", - thumbnail != null && thumbnail.asPointer().getKey() != null ? Base64.encodeWithPadding(thumbnail.asPointer().getKey()) : null, - null, - thumbnail != null ? thumbnail.asPointer().getDigest().orElse(null) : null, - thumbnail != null ? thumbnail.asPointer().getIncrementalDigest().orElse(null) : null, - thumbnail != null ? thumbnail.asPointer().getIncrementalMacChunkSize() : 0, - null, - false, - false, - false, - thumbnail != null ? thumbnail.asPointer().getWidth() : 0, - thumbnail != null ? thumbnail.asPointer().getHeight() : 0, - thumbnail != null ? thumbnail.asPointer().getUploadTimestamp() : 0, - thumbnail != null ? thumbnail.asPointer().getCaption().orElse(null) : null, - null, - null)); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/PointerAttachment.kt b/app/src/main/java/org/thoughtcrime/securesms/attachments/PointerAttachment.kt new file mode 100644 index 0000000000..003ca5ecdd --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/PointerAttachment.kt @@ -0,0 +1,192 @@ +package org.thoughtcrime.securesms.attachments + +import android.net.Uri +import android.os.Parcel +import org.signal.core.util.Base64.encodeWithPadding +import org.thoughtcrime.securesms.blurhash.BlurHash +import org.thoughtcrime.securesms.database.AttachmentTable +import org.thoughtcrime.securesms.stickers.StickerLocator +import org.whispersystems.signalservice.api.InvalidMessageStructureException +import org.whispersystems.signalservice.api.messages.SignalServiceAttachment +import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage +import org.whispersystems.signalservice.api.util.AttachmentPointerUtil +import org.whispersystems.signalservice.internal.push.DataMessage +import java.util.Optional + +class PointerAttachment : Attachment { + private constructor( + contentType: String, + transferState: Int, + size: Long, + fileName: String?, + cdnNumber: Int, + location: String, + key: String?, + relay: String?, + digest: ByteArray?, + incrementalDigest: ByteArray?, + incrementalMacChunkSize: Int, + fastPreflightId: String?, + voiceNote: Boolean, + borderless: Boolean, + videoGif: Boolean, + width: Int, + height: Int, + uploadTimestamp: Long, + caption: String?, + stickerLocator: StickerLocator?, + blurHash: BlurHash? + ) : super( + contentType = contentType, + transferState = transferState, + size = size, + fileName = fileName, + cdnNumber = cdnNumber, + location = location, + key = key, + relay = relay, + digest = digest, + incrementalDigest = incrementalDigest, + fastPreflightId = fastPreflightId, + voiceNote = voiceNote, + borderless = borderless, + videoGif = videoGif, + width = width, + height = height, + incrementalMacChunkSize = incrementalMacChunkSize, + quote = false, + uploadTimestamp = uploadTimestamp, + caption = caption, + stickerLocator = stickerLocator, + blurHash = blurHash, + audioHash = null, + transformProperties = null + ) + + constructor(parcel: Parcel) : super(parcel) + + override val uri: Uri? = null + override val publicUri: Uri? = null + + companion object { + @JvmStatic + fun forPointers(pointers: Optional>): List { + if (!pointers.isPresent) { + return emptyList() + } + + return pointers.get() + .map { forPointer(Optional.ofNullable(it)) } + .filter { it.isPresent } + .map { it.get() } + } + + @JvmStatic + @JvmOverloads + fun forPointer(pointer: Optional, stickerLocator: StickerLocator? = null, fastPreflightId: String? = null): Optional { + if (!pointer.isPresent || !pointer.get().isPointer) { + return Optional.empty() + } + + val encodedKey: String? = if (pointer.get().asPointer().key != null) { + encodeWithPadding(pointer.get().asPointer().key) + } else { + null + } + + return Optional.of( + PointerAttachment( + contentType = pointer.get().contentType, + transferState = AttachmentTable.TRANSFER_PROGRESS_PENDING, + size = pointer.get().asPointer().size.orElse(0).toLong(), + fileName = pointer.get().asPointer().fileName.orElse(null), + cdnNumber = pointer.get().asPointer().cdnNumber, + location = pointer.get().asPointer().remoteId.toString(), + key = encodedKey, + relay = null, + digest = pointer.get().asPointer().digest.orElse(null), + incrementalDigest = pointer.get().asPointer().incrementalDigest.orElse(null), + incrementalMacChunkSize = pointer.get().asPointer().incrementalMacChunkSize, + fastPreflightId = fastPreflightId, + voiceNote = pointer.get().asPointer().voiceNote, + borderless = pointer.get().asPointer().isBorderless, + videoGif = pointer.get().asPointer().isGif, + width = pointer.get().asPointer().width, + height = pointer.get().asPointer().height, + uploadTimestamp = pointer.get().asPointer().uploadTimestamp, + caption = pointer.get().asPointer().caption.orElse(null), + stickerLocator = stickerLocator, + blurHash = BlurHash.parseOrNull(pointer.get().asPointer().blurHash.orElse(null)) + ) + ) + } + + fun forPointer(pointer: SignalServiceDataMessage.Quote.QuotedAttachment): Optional { + val thumbnail = pointer.thumbnail + + return Optional.of( + PointerAttachment( + contentType = pointer.contentType, + transferState = AttachmentTable.TRANSFER_PROGRESS_PENDING, + size = (if (thumbnail != null) thumbnail.asPointer().size.orElse(0) else 0).toLong(), + fileName = pointer.fileName, + cdnNumber = thumbnail?.asPointer()?.cdnNumber ?: 0, + location = thumbnail?.asPointer()?.remoteId?.toString() ?: "0", + key = if (thumbnail != null && thumbnail.asPointer().key != null) encodeWithPadding(thumbnail.asPointer().key) else null, + relay = null, + digest = thumbnail?.asPointer()?.digest?.orElse(null), + incrementalDigest = thumbnail?.asPointer()?.incrementalDigest?.orElse(null), + incrementalMacChunkSize = thumbnail?.asPointer()?.incrementalMacChunkSize ?: 0, + fastPreflightId = null, + voiceNote = false, + borderless = false, + videoGif = false, + width = thumbnail?.asPointer()?.width ?: 0, + height = thumbnail?.asPointer()?.height ?: 0, + uploadTimestamp = thumbnail?.asPointer()?.uploadTimestamp ?: 0, + caption = thumbnail?.asPointer()?.caption?.orElse(null), + stickerLocator = null, + blurHash = null + ) + ) + } + + fun forPointer(quotedAttachment: DataMessage.Quote.QuotedAttachment): Optional { + val thumbnail: SignalServiceAttachment? = try { + if (quotedAttachment.thumbnail != null) { + AttachmentPointerUtil.createSignalAttachmentPointer(quotedAttachment.thumbnail) + } else { + null + } + } catch (e: InvalidMessageStructureException) { + return Optional.empty() + } + + return Optional.of( + PointerAttachment( + contentType = quotedAttachment.contentType!!, + transferState = AttachmentTable.TRANSFER_PROGRESS_PENDING, + size = (if (thumbnail != null) thumbnail.asPointer().size.orElse(0) else 0).toLong(), + fileName = quotedAttachment.fileName, + cdnNumber = thumbnail?.asPointer()?.cdnNumber ?: 0, + location = thumbnail?.asPointer()?.remoteId?.toString() ?: "0", + key = if (thumbnail != null && thumbnail.asPointer().key != null) encodeWithPadding(thumbnail.asPointer().key) else null, + relay = null, + digest = thumbnail?.asPointer()?.digest?.orElse(null), + incrementalDigest = thumbnail?.asPointer()?.incrementalDigest?.orElse(null), + incrementalMacChunkSize = thumbnail?.asPointer()?.incrementalMacChunkSize ?: 0, + fastPreflightId = null, + voiceNote = false, + borderless = false, + videoGif = false, + width = thumbnail?.asPointer()?.width ?: 0, + height = thumbnail?.asPointer()?.height ?: 0, + uploadTimestamp = thumbnail?.asPointer()?.uploadTimestamp ?: 0, + caption = thumbnail?.asPointer()?.caption?.orElse(null), + stickerLocator = null, + blurHash = null + ) + ) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/TombstoneAttachment.java b/app/src/main/java/org/thoughtcrime/securesms/attachments/TombstoneAttachment.java deleted file mode 100644 index 43d89ae7cf..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/TombstoneAttachment.java +++ /dev/null @@ -1,36 +0,0 @@ -package org.thoughtcrime.securesms.attachments; - -import android.net.Uri; -import android.os.Parcel; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.thoughtcrime.securesms.database.AttachmentTable; - -/** - * An attachment that represents where an attachment used to be. Useful when you need to know that - * a message had an attachment and some metadata about it (like the contentType), even though the - * underlying media no longer exists. An example usecase would be view-once messages, so that we can - * quote them and know their contentType even though the media has been deleted. - */ -public class TombstoneAttachment extends Attachment { - - public TombstoneAttachment(@NonNull String contentType, boolean quote) { - super(contentType, AttachmentTable.TRANSFER_PROGRESS_DONE, 0, null, 0, null, null, null, null, null, null, false, false, false, 0, 0, 0, quote, 0, null, null, null, null, null); - } - - protected TombstoneAttachment(Parcel in) { - super(in); - } - - @Override - public @Nullable Uri getUri() { - return null; - } - - @Override - public @Nullable Uri getPublicUri() { - return null; - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/TombstoneAttachment.kt b/app/src/main/java/org/thoughtcrime/securesms/attachments/TombstoneAttachment.kt new file mode 100644 index 0000000000..857c96f03d --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/TombstoneAttachment.kt @@ -0,0 +1,45 @@ +package org.thoughtcrime.securesms.attachments + +import android.net.Uri +import android.os.Parcel +import org.thoughtcrime.securesms.database.AttachmentTable + +/** + * An attachment that represents where an attachment used to be. Useful when you need to know that + * a message had an attachment and some metadata about it (like the contentType), even though the + * underlying media no longer exists. An example usecase would be view-once messages, so that we can + * quote them and know their contentType even though the media has been deleted. + */ +class TombstoneAttachment : Attachment { + constructor(contentType: String, quote: Boolean) : super( + contentType = contentType, + quote = quote, + transferState = AttachmentTable.TRANSFER_PROGRESS_DONE, + size = 0, + fileName = null, + cdnNumber = 0, + location = null, + key = null, + relay = null, + digest = null, + incrementalDigest = null, + fastPreflightId = null, + voiceNote = false, + borderless = false, + videoGif = false, + width = 0, + height = 0, + incrementalMacChunkSize = 0, + uploadTimestamp = 0, + caption = null, + stickerLocator = null, + blurHash = null, + audioHash = null, + transformProperties = null + ) + + constructor(parcel: Parcel) : super(parcel) + + override val uri: Uri? = null + override val publicUri: Uri? = null +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/UriAttachment.java b/app/src/main/java/org/thoughtcrime/securesms/attachments/UriAttachment.java deleted file mode 100644 index eb0c7bcd26..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/UriAttachment.java +++ /dev/null @@ -1,92 +0,0 @@ -package org.thoughtcrime.securesms.attachments; - -import android.net.Uri; -import android.os.Parcel; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.os.ParcelCompat; - -import org.thoughtcrime.securesms.audio.AudioHash; -import org.thoughtcrime.securesms.blurhash.BlurHash; -import org.thoughtcrime.securesms.database.AttachmentTable.TransformProperties; -import org.thoughtcrime.securesms.stickers.StickerLocator; - -import java.util.Objects; - -public class UriAttachment extends Attachment { - - private final @NonNull Uri dataUri; - - public UriAttachment(@NonNull Uri uri, - @NonNull String contentType, - int transferState, - long size, - @Nullable String fileName, - boolean voiceNote, - boolean borderless, - boolean videoGif, - boolean quote, - @Nullable String caption, - @Nullable StickerLocator stickerLocator, - @Nullable BlurHash blurHash, - @Nullable AudioHash audioHash, - @Nullable TransformProperties transformProperties) - { - this(uri, contentType, transferState, size, 0, 0, fileName, null, voiceNote, borderless, videoGif, quote, caption, stickerLocator, blurHash, audioHash, transformProperties); - } - - public UriAttachment(@NonNull Uri dataUri, - @NonNull String contentType, - int transferState, - long size, - int width, - int height, - @Nullable String fileName, - @Nullable String fastPreflightId, - boolean voiceNote, - boolean borderless, - boolean videoGif, - boolean quote, - @Nullable String caption, - @Nullable StickerLocator stickerLocator, - @Nullable BlurHash blurHash, - @Nullable AudioHash audioHash, - @Nullable TransformProperties transformProperties) - { - super(contentType, transferState, size, fileName, 0, null, null, null, null, null, fastPreflightId, voiceNote, borderless, videoGif, width, height, 0, quote, 0, caption, stickerLocator, blurHash, audioHash, transformProperties); - this.dataUri = Objects.requireNonNull(dataUri); - } - - protected UriAttachment(Parcel in) { - super(in); - this.dataUri = Objects.requireNonNull(ParcelCompat.readParcelable(in, Uri.class.getClassLoader(), Uri.class)); - } - - @Override - public void writeToParcel(@NonNull Parcel dest, int flags) { - super.writeToParcel(dest, flags); - dest.writeParcelable(dataUri, 0); - } - - @Override - @NonNull - public Uri getUri() { - return dataUri; - } - - @Override - public @Nullable Uri getPublicUri() { - return null; - } - - @Override - public boolean equals(Object other) { - return other != null && other instanceof UriAttachment && ((UriAttachment) other).dataUri.equals(this.dataUri); - } - - @Override - public int hashCode() { - return dataUri.hashCode(); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/UriAttachment.kt b/app/src/main/java/org/thoughtcrime/securesms/attachments/UriAttachment.kt new file mode 100644 index 0000000000..800707b053 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/UriAttachment.kt @@ -0,0 +1,115 @@ +package org.thoughtcrime.securesms.attachments + +import android.net.Uri +import android.os.Parcel +import androidx.core.os.ParcelCompat +import org.thoughtcrime.securesms.audio.AudioHash +import org.thoughtcrime.securesms.blurhash.BlurHash +import org.thoughtcrime.securesms.database.AttachmentTable.TransformProperties +import org.thoughtcrime.securesms.stickers.StickerLocator +import java.util.Objects + +class UriAttachment : Attachment { + + constructor( + uri: Uri, + contentType: String, + transferState: Int, + size: Long, + fileName: String?, + voiceNote: Boolean, + borderless: Boolean, + videoGif: Boolean, + quote: Boolean, + caption: String?, + stickerLocator: StickerLocator?, + blurHash: BlurHash?, + audioHash: AudioHash?, + transformProperties: TransformProperties? + ) : this( + dataUri = uri, + contentType = contentType, + transferState = transferState, + size = size, + width = 0, + height = 0, + fileName = fileName, + fastPreflightId = null, + voiceNote = voiceNote, + borderless = borderless, + videoGif = videoGif, + quote = quote, + caption = caption, + stickerLocator = stickerLocator, + blurHash = blurHash, + audioHash = audioHash, + transformProperties = transformProperties + ) + + constructor( + dataUri: Uri, + contentType: String, + transferState: Int, + size: Long, + width: Int, + height: Int, + fileName: String?, + fastPreflightId: String?, + voiceNote: Boolean, + borderless: Boolean, + videoGif: Boolean, + quote: Boolean, + caption: String?, + stickerLocator: StickerLocator?, + blurHash: BlurHash?, + audioHash: AudioHash?, + transformProperties: TransformProperties? + ) : super( + contentType = contentType, + transferState = transferState, + size = size, + fileName = fileName, + cdnNumber = 0, + location = null, + key = null, + relay = null, + digest = null, + incrementalDigest = null, + fastPreflightId = fastPreflightId, + voiceNote = voiceNote, + borderless = borderless, + videoGif = videoGif, + width = width, + height = height, + incrementalMacChunkSize = 0, + quote = quote, + uploadTimestamp = 0, + caption = caption, + stickerLocator = stickerLocator, + blurHash = blurHash, + audioHash = audioHash, + transformProperties = transformProperties + ) { + uri = Objects.requireNonNull(dataUri) + } + + 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 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 UriAttachment && other.uri == uri + } + + override fun hashCode(): Int { + return uri.hashCode() + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/BorderlessImageView.java b/app/src/main/java/org/thoughtcrime/securesms/components/BorderlessImageView.java index 534ea159b0..2366c4ef15 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/BorderlessImageView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/BorderlessImageView.java @@ -63,7 +63,7 @@ public class BorderlessImageView extends FrameLayout { image.setImageResource(glideRequests, slide, showControls, false); } else { image.setScaleType(ImageView.ScaleType.CENTER_CROP); - image.setImageResource(glideRequests, slide, showControls, false, slide.asAttachment().getWidth(), slide.asAttachment().getHeight()); + image.setImageResource(glideRequests, slide, showControls, false, slide.asAttachment().width, slide.asAttachment().height); } missingShade.setVisibility(showControls ? View.VISIBLE : View.GONE); diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/ThumbnailView.java b/app/src/main/java/org/thoughtcrime/securesms/components/ThumbnailView.java index 369e8a06a3..4cf5a55330 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/ThumbnailView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/ThumbnailView.java @@ -397,7 +397,7 @@ public class ThumbnailView extends FrameLayout { Log.i(TAG, "loading part with id " + slide.asAttachment().getUri() + ", progress " + slide.getTransferState() + ", fast preflight id: " + - slide.asAttachment().getFastPreflightId()); + slide.asAttachment().fastPreflightId); BlurHash previousBlurHash = this.slide != null ? this.slide.getPlaceholderBlur() : null; @@ -532,8 +532,8 @@ public class ThumbnailView extends FrameLayout { if (Util.equals(slide, other)) { if (slide != null && other != null) { - byte[] digestLeft = slide.asAttachment().getDigest(); - byte[] digestRight = other.asAttachment().getDigest(); + byte[] digestLeft = slide.asAttachment().digest; + byte[] digestRight = other.asAttachment().digest; return Arrays.equals(digestLeft, digestRight); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java index f6808a2041..c19fdf921b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java @@ -2023,10 +2023,10 @@ public final class ConversationItem extends RelativeLayout implements BindableCo if (messageRecord.isMms()) { TextSlide slide = ((MmsMessageRecord) messageRecord).getSlideDeck().getTextSlide(); - if (slide != null && (slide.asAttachment().getTransferState() == AttachmentTable.TRANSFER_PROGRESS_DONE || MessageRecordUtil.isScheduled(messageRecord))) { + if (slide != null && (slide.asAttachment().transferState == AttachmentTable.TRANSFER_PROGRESS_DONE || MessageRecordUtil.isScheduled(messageRecord))) { message = getResources().getString(R.string.ConversationItem_read_more); action = () -> eventListener.onMoreTextClicked(conversationRecipient.getId(), messageRecord.getId(), messageRecord.isMms()); - } else if (slide != null && slide.asAttachment().getTransferState() == AttachmentTable.TRANSFER_PROGRESS_STARTED) { + } else if (slide != null && slide.asAttachment().transferState == AttachmentTable.TRANSFER_PROGRESS_STARTED) { message = getResources().getString(R.string.ConversationItem_pending); action = () -> {}; } else if (slide != null) { @@ -2438,7 +2438,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo for (Slide slide : slides) { ApplicationDependencies.getJobManager().add(new AttachmentDownloadJob(messageRecord.getId(), - ((DatabaseAttachment) slide.asAttachment()).getAttachmentId(), + ((DatabaseAttachment) slide.asAttachment()).attachmentId, true)); } } @@ -2457,7 +2457,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo Log.i(TAG, "Canceling push attachment downloads for " + slides.size() + " items"); for (Slide slide : slides) { - final String queue = AttachmentDownloadJob.constructQueueString(((DatabaseAttachment) slide.asAttachment()).getAttachmentId()); + final String queue = AttachmentDownloadJob.constructQueueString(((DatabaseAttachment) slide.asAttachment()).attachmentId); jobManager.cancelAllInQueue(queue); } } @@ -2477,8 +2477,8 @@ public final class ConversationItem extends RelativeLayout implements BindableCo } if (MediaUtil.isInstantVideoSupported(slide)) { final DatabaseAttachment databaseAttachment = (DatabaseAttachment) slide.asAttachment(); - if (databaseAttachment.getTransferState() != AttachmentTable.TRANSFER_PROGRESS_STARTED) { - final AttachmentId attachmentId = databaseAttachment.getAttachmentId(); + if (databaseAttachment.transferState != AttachmentTable.TRANSFER_PROGRESS_STARTED) { + final AttachmentId attachmentId = databaseAttachment.attachmentId; final JobManager jobManager = ApplicationDependencies.getJobManager(); final String queue = AttachmentDownloadJob.constructQueueString(attachmentId); setup(v, slide); @@ -2550,7 +2550,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo performClick(); } else if (eventListener != null && hasSticker(messageRecord)) { //noinspection ConstantConditions - eventListener.onStickerClicked(((MmsMessageRecord) messageRecord).getSlideDeck().getStickerSlide().asAttachment().getSticker()); + eventListener.onStickerClicked(((MmsMessageRecord) messageRecord).getSlideDeck().getStickerSlide().asAttachment().stickerLocator); } } } @@ -2617,7 +2617,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo messageRecord.getTimestamp(), mediaUri, slide.getContentType(), - slide.asAttachment().getSize(), + slide.asAttachment().size, slide.getCaption().orElse(null), false, false, @@ -2626,8 +2626,8 @@ public final class ConversationItem extends RelativeLayout implements BindableCo MediaTable.Sorting.Newest, slide.isVideoGif(), new MediaIntentFactory.SharedElementArgs( - slide.asAttachment().getWidth(), - slide.asAttachment().getHeight(), + slide.asAttachment().width, + slide.asAttachment().height, mediaThumbnailStub.require().getCorners().getTopLeft(), mediaThumbnailStub.require().getCorners().getTopRight(), mediaThumbnailStub.require().getCorners().getBottomRight(), diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardFragmentArgs.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardFragmentArgs.kt index c6cc81f38c..d13aa0c441 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardFragmentArgs.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardFragmentArgs.kt @@ -175,7 +175,7 @@ data class MultiselectForwardFragmentArgs @JvmOverloads constructor( if (mediaMessage.slideDeck.stickerSlide != null) { builder.withDataUri(mediaMessage.slideDeck.stickerSlide?.asAttachment()?.uri) - builder.withStickerLocator(mediaMessage.slideDeck.stickerSlide?.asAttachment()?.sticker) + builder.withStickerLocator(mediaMessage.slideDeck.stickerSlide?.asAttachment()?.stickerLocator) builder.withDataType(mediaMessage.slideDeck.stickerSlide?.asAttachment()?.contentType) } @@ -203,11 +203,11 @@ data class MultiselectForwardFragmentArgs @JvmOverloads constructor( height, size, 0, - isBorderless, - isVideoGif, + borderless, + videoGif, Optional.empty(), Optional.ofNullable(caption), - Optional.of(transformProperties) + Optional.ofNullable(transformProperties) ) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentTable.java b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentTable.java deleted file mode 100644 index 55e782f23b..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentTable.java +++ /dev/null @@ -1,1806 +0,0 @@ -/* - * Copyright (C) 2011 Whisper Systems - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.thoughtcrime.securesms.database; - -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; -import android.media.MediaDataSource; -import android.os.Parcel; -import android.os.Parcelable; -import android.text.TextUtils; -import android.util.Pair; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.RequiresApi; -import androidx.annotation.VisibleForTesting; -import androidx.annotation.WorkerThread; - -import com.bumptech.glide.Glide; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; - -import org.json.JSONArray; -import org.json.JSONException; -import org.signal.core.util.Base64; -import org.signal.core.util.CursorExtensionsKt; -import org.signal.core.util.CursorUtil; -import org.signal.core.util.SQLiteDatabaseExtensionsKt; -import org.signal.core.util.SetUtil; -import org.signal.core.util.SqlUtil; -import org.signal.core.util.StreamUtil; -import org.signal.core.util.ThreadUtil; -import org.signal.core.util.logging.Log; -import org.thoughtcrime.securesms.attachments.Attachment; -import org.thoughtcrime.securesms.attachments.AttachmentId; -import org.thoughtcrime.securesms.attachments.DatabaseAttachment; -import org.thoughtcrime.securesms.audio.AudioHash; -import org.thoughtcrime.securesms.blurhash.BlurHash; -import org.thoughtcrime.securesms.crypto.AttachmentSecret; -import org.thoughtcrime.securesms.crypto.ClassicDecryptingPartInputStream; -import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream; -import org.thoughtcrime.securesms.crypto.ModernEncryptingPartOutputStream; -import org.thoughtcrime.securesms.database.model.databaseprotos.AudioWaveFormData; -import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; -import org.thoughtcrime.securesms.jobmanager.JobManager; -import org.thoughtcrime.securesms.jobs.AttachmentDownloadJob; -import org.thoughtcrime.securesms.jobs.GenerateAudioWaveFormJob; -import org.thoughtcrime.securesms.mms.MediaStream; -import org.thoughtcrime.securesms.mms.MmsException; -import org.thoughtcrime.securesms.mms.PartAuthority; -import org.thoughtcrime.securesms.mms.SentMediaQuality; -import org.thoughtcrime.securesms.stickers.StickerLocator; -import org.thoughtcrime.securesms.util.FileUtils; -import org.thoughtcrime.securesms.util.JsonUtils; -import org.thoughtcrime.securesms.util.MediaUtil; -import org.thoughtcrime.securesms.util.StorageUtil; -import org.thoughtcrime.securesms.video.EncryptedMediaDataSource; -import org.whispersystems.signalservice.internal.util.JsonUtil; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.security.DigestInputStream; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; - -public class AttachmentTable extends DatabaseTable { - - public static final String TAG = Log.tag(AttachmentTable.class); - - public static final String TABLE_NAME = "part"; - public static final String ROW_ID = "_id"; - static final String ATTACHMENT_JSON_ALIAS = "attachment_json"; - public static final String MMS_ID = "mid"; - static final String CONTENT_TYPE = "ct"; - static final String NAME = "name"; - static final String CONTENT_DISPOSITION = "cd"; - static final String CONTENT_LOCATION = "cl"; - public static final String DATA = "_data"; - static final String TRANSFER_STATE = "pending_push"; - public static final String TRANSFER_FILE = "transfer_file"; - public static final String SIZE = "data_size"; - static final String FILE_NAME = "file_name"; - public static final String UNIQUE_ID = "unique_id"; - static final String DIGEST = "digest"; - static final String VOICE_NOTE = "voice_note"; - static final String BORDERLESS = "borderless"; - static final String VIDEO_GIF = "video_gif"; - static final String QUOTE = "quote"; - public static final String STICKER_PACK_ID = "sticker_pack_id"; - public static final String STICKER_PACK_KEY = "sticker_pack_key"; - static final String STICKER_ID = "sticker_id"; - static final String STICKER_EMOJI = "sticker_emoji"; - static final String FAST_PREFLIGHT_ID = "fast_preflight_id"; - public static final String DATA_RANDOM = "data_random"; - static final String WIDTH = "width"; - static final String HEIGHT = "height"; - static final String CAPTION = "caption"; - static final String DATA_HASH = "data_hash"; - static final String VISUAL_HASH = "blur_hash"; - static final String TRANSFORM_PROPERTIES = "transform_properties"; - static final String DISPLAY_ORDER = "display_order"; - static final String UPLOAD_TIMESTAMP = "upload_timestamp"; - static final String CDN_NUMBER = "cdn_number"; - static final String MAC_DIGEST = "incremental_mac_digest"; - static final String INCREMENTAL_MAC_CHUNK_SIZE = "incremental_mac_chunk_size"; - - private static final String DIRECTORY = "parts"; - - public static final int TRANSFER_PROGRESS_DONE = 0; - public static final int TRANSFER_PROGRESS_STARTED = 1; - public static final int TRANSFER_PROGRESS_PENDING = 2; - public static final int TRANSFER_PROGRESS_FAILED = 3; - public static final int TRANSFER_PROGRESS_PERMANENT_FAILURE = 4; - - public static final long PREUPLOAD_MESSAGE_ID = -8675309; - - private static final String PART_ID_WHERE = ROW_ID + " = ? AND " + UNIQUE_ID + " = ?"; - private static final String PART_ID_WHERE_NOT = ROW_ID + " != ? AND " + UNIQUE_ID + " != ?"; - - private static final String[] PROJECTION = new String[] {ROW_ID, - MMS_ID, CONTENT_TYPE, NAME, CONTENT_DISPOSITION, - CDN_NUMBER, CONTENT_LOCATION, DATA, - TRANSFER_STATE, SIZE, FILE_NAME, UNIQUE_ID, DIGEST, MAC_DIGEST, INCREMENTAL_MAC_CHUNK_SIZE, - FAST_PREFLIGHT_ID, VOICE_NOTE, BORDERLESS, VIDEO_GIF, QUOTE, DATA_RANDOM, - WIDTH, HEIGHT, CAPTION, STICKER_PACK_ID, - STICKER_PACK_KEY, STICKER_ID, STICKER_EMOJI, DATA_HASH, VISUAL_HASH, - TRANSFORM_PROPERTIES, TRANSFER_FILE, DISPLAY_ORDER, - UPLOAD_TIMESTAMP }; - - public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ROW_ID + " INTEGER PRIMARY KEY, " + - MMS_ID + " INTEGER, " + - "seq" + " INTEGER DEFAULT 0, " + - CONTENT_TYPE + " TEXT, " + - NAME + " TEXT, " + - "chset" + " INTEGER, " + - CONTENT_DISPOSITION + " TEXT, " + - "fn" + " TEXT, " + - "cid" + " TEXT, " + - CONTENT_LOCATION + " TEXT, " + - "ctt_s" + " INTEGER, " + - "ctt_t" + " TEXT, " + - "encrypted" + " INTEGER, " + - TRANSFER_STATE + " INTEGER, " + - DATA + " TEXT, " + - SIZE + " INTEGER, " + - FILE_NAME + " TEXT, " + - UNIQUE_ID + " INTEGER NOT NULL, " + - DIGEST + " BLOB, " + - FAST_PREFLIGHT_ID + " TEXT, " + - VOICE_NOTE + " INTEGER DEFAULT 0, " + - BORDERLESS + " INTEGER DEFAULT 0, " + - VIDEO_GIF + " INTEGER DEFAULT 0, " + - DATA_RANDOM + " BLOB, " + - QUOTE + " INTEGER DEFAULT 0, " + - WIDTH + " INTEGER DEFAULT 0, " + - HEIGHT + " INTEGER DEFAULT 0, " + - CAPTION + " TEXT DEFAULT NULL, " + - STICKER_PACK_ID + " TEXT DEFAULT NULL, " + - STICKER_PACK_KEY + " DEFAULT NULL, " + - STICKER_ID + " INTEGER DEFAULT -1, " + - STICKER_EMOJI + " STRING DEFAULT NULL, " + - DATA_HASH + " TEXT DEFAULT NULL, " + - VISUAL_HASH + " TEXT DEFAULT NULL, " + - TRANSFORM_PROPERTIES + " TEXT DEFAULT NULL, " + - TRANSFER_FILE + " TEXT DEFAULT NULL, " + - DISPLAY_ORDER + " INTEGER DEFAULT 0, " + - UPLOAD_TIMESTAMP + " INTEGER DEFAULT 0, " + - CDN_NUMBER + " INTEGER DEFAULT 0, " + - MAC_DIGEST + " BLOB, " + - INCREMENTAL_MAC_CHUNK_SIZE + " INTEGER DEFAULT 0);"; - - public static final String[] CREATE_INDEXS = { - "CREATE INDEX IF NOT EXISTS part_mms_id_index ON " + TABLE_NAME + " (" + MMS_ID + ");", - "CREATE INDEX IF NOT EXISTS pending_push_index ON " + TABLE_NAME + " (" + TRANSFER_STATE + ");", - "CREATE INDEX IF NOT EXISTS part_sticker_pack_id_index ON " + TABLE_NAME + " (" + STICKER_PACK_ID + ");", - "CREATE INDEX IF NOT EXISTS part_data_hash_index ON " + TABLE_NAME + " (" + DATA_HASH + ");", - "CREATE INDEX IF NOT EXISTS part_data_index ON " + TABLE_NAME + " (" + DATA + ");" - }; - - private final AttachmentSecret attachmentSecret; - - public AttachmentTable(Context context, SignalDatabase databaseHelper, AttachmentSecret attachmentSecret) { - super(context, databaseHelper); - this.attachmentSecret = attachmentSecret; - } - - public @NonNull InputStream getAttachmentStream(AttachmentId attachmentId, long offset) - throws IOException - { - InputStream dataStream; - - try { - dataStream = getDataStream(attachmentId, DATA, offset); - } catch (FileNotFoundException e) { - throw new IOException("No stream for: " + attachmentId, e); - } - - if (dataStream == null) throw new IOException("No stream for: " + attachmentId); - else return dataStream; - } - - public boolean containsStickerPackId(@NonNull String stickerPackId) { - String selection = STICKER_PACK_ID + " = ?"; - String[] args = new String[] { stickerPackId }; - - try (Cursor cursor = databaseHelper.getSignalReadableDatabase().query(TABLE_NAME, null, selection, args, null, null, null, "1")) { - return cursor != null && cursor.moveToFirst(); - } - } - - public void setTransferProgressFailed(AttachmentId attachmentId, long mmsId) - throws MmsException - { - SQLiteDatabase database = databaseHelper.getSignalWritableDatabase(); - ContentValues values = new ContentValues(); - values.put(TRANSFER_STATE, TRANSFER_PROGRESS_FAILED); - - database.update(TABLE_NAME, values, PART_ID_WHERE + " AND " + TRANSFER_STATE + " < " + TRANSFER_PROGRESS_PERMANENT_FAILURE, attachmentId.toStrings()); - notifyConversationListeners(SignalDatabase.messages().getThreadIdForMessage(mmsId)); - } - - public void setTransferProgressPermanentFailure(AttachmentId attachmentId, long mmsId) - throws MmsException - { - SQLiteDatabase database = databaseHelper.getSignalWritableDatabase(); - ContentValues values = new ContentValues(); - values.put(TRANSFER_STATE, TRANSFER_PROGRESS_PERMANENT_FAILURE); - - database.update(TABLE_NAME, values, PART_ID_WHERE, attachmentId.toStrings()); - notifyConversationListeners(SignalDatabase.messages().getThreadIdForMessage(mmsId)); - } - - public @Nullable DatabaseAttachment getAttachment(@NonNull AttachmentId attachmentId) - { - SQLiteDatabase database = databaseHelper.getSignalReadableDatabase(); - Cursor cursor = null; - - try { - cursor = database.query(TABLE_NAME, PROJECTION, PART_ID_WHERE, attachmentId.toStrings(), null, null, null); - - if (cursor != null && cursor.moveToFirst()) { - List list = getAttachments(cursor); - - if (list != null && list.size() > 0) { - return list.get(0); - } - } - - return null; - } finally { - if (cursor != null) - cursor.close(); - } - } - - public @NonNull List getAttachmentsForMessage(long mmsId) { - SQLiteDatabase database = databaseHelper.getSignalReadableDatabase(); - List results = new LinkedList<>(); - Cursor cursor = null; - - try { - cursor = database.query(TABLE_NAME, PROJECTION, MMS_ID + " = ?", new String[] {mmsId+""}, - null, null, UNIQUE_ID + " ASC, " + ROW_ID + " ASC"); - - while (cursor != null && cursor.moveToNext()) { - results.addAll(getAttachments(cursor)); - } - - return results; - } finally { - if (cursor != null) - cursor.close(); - } - } - - public @NonNull Map> getAttachmentsForMessages(@NonNull Collection mmsIds) { - if (mmsIds.isEmpty()) { - return Collections.emptyMap(); - } - - SQLiteDatabase database = databaseHelper.getSignalReadableDatabase(); - SqlUtil.Query query = SqlUtil.buildSingleCollectionQuery(MMS_ID, mmsIds); - - Map> output = new HashMap<>(); - - try (Cursor cursor = database.query(TABLE_NAME, PROJECTION, query.getWhere(), query.getWhereArgs(), null, null, UNIQUE_ID + " ASC, " + ROW_ID + " ASC")) { - while (cursor.moveToNext()) { - DatabaseAttachment attachment = getAttachment(cursor); - List attachments = output.get(attachment.getMmsId()); - - if (attachments == null) { - attachments = new LinkedList<>(); - output.put(attachment.getMmsId(), attachments); - } - - attachments.add(attachment); - } - } - - return output; - } - - public boolean hasAttachment(@NonNull AttachmentId id) { - SQLiteDatabase database = databaseHelper.getSignalReadableDatabase(); - - try (Cursor cursor = database.query(TABLE_NAME, - new String[]{ROW_ID, UNIQUE_ID}, - PART_ID_WHERE, - id.toStrings(), - null, - null, - null)) { - if (cursor != null && cursor.getCount() > 0) { - return true; - } - } - return false; - } - - public @NonNull List getPendingAttachments() { - final SQLiteDatabase database = databaseHelper.getSignalReadableDatabase(); - final List attachments = new LinkedList<>(); - - Cursor cursor = null; - try { - cursor = database.query(TABLE_NAME, PROJECTION, TRANSFER_STATE + " = ?", new String[] {String.valueOf(TRANSFER_PROGRESS_STARTED)}, null, null, null); - while (cursor != null && cursor.moveToNext()) { - attachments.addAll(getAttachments(cursor)); - } - } finally { - if (cursor != null) cursor.close(); - } - - return attachments; - } - - public boolean deleteAttachmentsForMessage(long mmsId) { - Log.d(TAG, "[deleteAttachmentsForMessage] mmsId: " + mmsId); - - final JobManager jobManager = ApplicationDependencies.getJobManager(); - - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - db.beginTransaction(); - try { - try (Cursor cursor = db.query(TABLE_NAME, new String[] { DATA, CONTENT_TYPE, ROW_ID, UNIQUE_ID }, MMS_ID + " = ?", new String[] { mmsId + "" }, null, null, null)) { - while (cursor.moveToNext()) { - final AttachmentId attachmentId = new AttachmentId(CursorUtil.requireLong(cursor, ROW_ID), - CursorUtil.requireLong(cursor, UNIQUE_ID)); - jobManager.cancelAllInQueue(AttachmentDownloadJob.constructQueueString(attachmentId)); - deleteAttachmentOnDisk(CursorUtil.requireString(cursor, DATA), - CursorUtil.requireString(cursor, CONTENT_TYPE), - attachmentId); - } - } - - int deleteCount = db.delete(TABLE_NAME, MMS_ID + " = ?", new String[] { mmsId + "" }); - notifyAttachmentListeners(); - db.setTransactionSuccessful(); - - return deleteCount > 0; - } finally { - db.endTransaction(); - } - } - - /** - * Deletes all attachments with an ID of {@link #PREUPLOAD_MESSAGE_ID}. These represent - * attachments that were pre-uploaded and haven't been assigned to a message. This should only be - * done when you *know* that all attachments *should* be assigned a real mmsId. For instance, when - * the app starts. Otherwise you could delete attachments that are legitimately being - * pre-uploaded. - */ - public int deleteAbandonedPreuploadedAttachments() { - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - String query = MMS_ID + " = ?"; - String[] args = new String[] { String.valueOf(PREUPLOAD_MESSAGE_ID) }; - int count = 0; - - try (Cursor cursor = db.query(TABLE_NAME, null, query, args, null, null, null)) { - while (cursor != null && cursor.moveToNext()) { - long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(ROW_ID)); - long uniqueId = cursor.getLong(cursor.getColumnIndexOrThrow(UNIQUE_ID)); - AttachmentId id = new AttachmentId(rowId, uniqueId); - - deleteAttachment(id); - count++; - } - } - - return count; - } - - public void deleteAttachmentFilesForViewOnceMessage(long mmsId) { - Log.d(TAG, "[deleteAttachmentFilesForViewOnceMessage] mmsId: " + mmsId); - - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - - db.beginTransaction(); - try { - try (Cursor cursor = db.query(TABLE_NAME, new String[] { DATA, CONTENT_TYPE, ROW_ID, UNIQUE_ID }, MMS_ID + " = ?", new String[] { mmsId + "" }, null, null, null)) { - while (cursor != null && cursor.moveToNext()) { - deleteAttachmentOnDisk(CursorUtil.requireString(cursor, DATA), - CursorUtil.requireString(cursor, CONTENT_TYPE), - new AttachmentId(CursorUtil.requireLong(cursor, ROW_ID), - CursorUtil.requireLong(cursor, UNIQUE_ID))); - } - } - - ContentValues values = new ContentValues(); - values.put(DATA, (String) null); - values.put(DATA_RANDOM, (byte[]) null); - values.put(DATA_HASH, (String) null); - values.put(FILE_NAME, (String) null); - values.put(CAPTION, (String) null); - values.put(SIZE, 0); - values.put(WIDTH, 0); - values.put(HEIGHT, 0); - values.put(TRANSFER_STATE, TRANSFER_PROGRESS_DONE); - values.put(VISUAL_HASH, (String) null); - values.put(CONTENT_TYPE, MediaUtil.VIEW_ONCE); - - db.update(TABLE_NAME, values, MMS_ID + " = ?", new String[] { mmsId + "" }); - notifyAttachmentListeners(); - - long threadId = SignalDatabase.messages().getThreadIdForMessage(mmsId); - if (threadId > 0) { - notifyConversationListeners(threadId); - } - - db.setTransactionSuccessful(); - } finally { - db.endTransaction(); - } - } - - public void deleteAttachment(@NonNull AttachmentId id) { - Log.d(TAG, "[deleteAttachment] attachmentId: " + id); - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - - db.beginTransaction(); - try { - try (Cursor cursor = db.query(TABLE_NAME, new String[]{DATA, CONTENT_TYPE}, PART_ID_WHERE, id.toStrings(), null, null, null)) { - if (!cursor.moveToNext()) { - Log.w(TAG, "Tried to delete an attachment, but it didn't exist."); - db.setTransactionSuccessful(); - return; - } - String data = CursorUtil.requireString(cursor, DATA); - String contentType = CursorUtil.requireString(cursor, CONTENT_TYPE); - - db.delete(TABLE_NAME, PART_ID_WHERE, id.toStrings()); - deleteAttachmentOnDisk(data, contentType, id); - notifyAttachmentListeners(); - db.setTransactionSuccessful(); - } - } finally { - db.endTransaction(); - } - } - - public void trimAllAbandonedAttachments() { - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - String selectAllMmsIds = "SELECT " + MessageTable.ID + " FROM " + MessageTable.TABLE_NAME; - String where = MMS_ID + " != " + PREUPLOAD_MESSAGE_ID + " AND " + MMS_ID + " NOT IN (" + selectAllMmsIds + ")"; - - int deletes = db.delete(TABLE_NAME, where, null); - if (deletes > 0) { - Log.i(TAG, "Trimmed " + deletes + " abandoned attachments."); - } - } - - public int deleteAbandonedAttachmentFiles() { - File[] diskFiles = context.getDir(DIRECTORY, Context.MODE_PRIVATE).listFiles(); - - if (diskFiles == null) { - return 0; - } - - Set filesOnDisk = Arrays.stream(diskFiles) - .filter(f -> !PartFileProtector.isProtected(f)) - .map(File::getAbsolutePath) - .collect(Collectors.toSet()); - - Set filesInDb = new HashSet<>(); - try (Cursor cursor = databaseHelper.getSignalReadableDatabase().query(true, TABLE_NAME, new String[] { DATA }, null, null, null, null, null, null)) { - while (cursor != null && cursor.moveToNext()) { - filesInDb.add(CursorUtil.requireString(cursor, DATA)); - } - } - - filesInDb.addAll(SignalDatabase.stickers().getAllStickerFiles()); - - Set onDiskButNotInDatabase = SetUtil.difference(filesOnDisk, filesInDb); - - for (String filePath : onDiskButNotInDatabase) { - //noinspection ResultOfMethodCallIgnored - new File(filePath).delete(); - } - - return onDiskButNotInDatabase.size(); - } - - @SuppressWarnings("ResultOfMethodCallIgnored") - void deleteAllAttachments() { - SQLiteDatabase database = databaseHelper.getSignalWritableDatabase(); - database.delete(TABLE_NAME, null, null); - - FileUtils.deleteDirectoryContents(context.getDir(DIRECTORY, Context.MODE_PRIVATE)); - - notifyAttachmentListeners(); - } - - private void deleteAttachmentOnDisk(@Nullable String data, - @Nullable String contentType, - @NonNull AttachmentId attachmentId) - { - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - - if (!db.inTransaction()) { - throw new IllegalStateException("Must be in a transaction!"); - } - - DataUsageResult dataUsage = getAttachmentFileUsages(data, attachmentId); - - if (dataUsage.hasStrongReference()) { - Log.i(TAG, "[deleteAttachmentOnDisk] Attachment in use. Skipping deletion. " + data + " " + attachmentId); - return; - } - - Log.i(TAG, "[deleteAttachmentOnDisk] No other strong uses of this attachment. Safe to delete. " + data + " " + attachmentId); - - if (!TextUtils.isEmpty(data)) { - if (new File(data).delete()) { - Log.i(TAG, "[deleteAttachmentOnDisk] Deleted attachment file. " + data + " " + attachmentId); - - List removableWeakReferences = dataUsage.getRemovableWeakReferences(); - - if (removableWeakReferences.size() > 0) { - Log.i(TAG, String.format(Locale.US, "[deleteAttachmentOnDisk] Deleting %d weak references for %s", removableWeakReferences.size(), data)); - int deletedCount = 0; - - for (AttachmentId weakReference : removableWeakReferences) { - Log.i(TAG, String.format("[deleteAttachmentOnDisk] Clearing weak reference for %s %s", data, weakReference)); - ContentValues values = new ContentValues(); - values.putNull(DATA); - values.putNull(DATA_RANDOM); - values.putNull(DATA_HASH); - deletedCount += db.update(TABLE_NAME, values, PART_ID_WHERE, weakReference.toStrings()); - } - - String logMessage = String.format(Locale.US, "[deleteAttachmentOnDisk] Cleared %d/%d weak references for %s", deletedCount, removableWeakReferences.size(), data); - if (deletedCount != removableWeakReferences.size()) { - Log.w(TAG, logMessage); - } else { - Log.i(TAG, logMessage); - } - } - } else { - Log.w(TAG, "[deleteAttachmentOnDisk] Failed to delete attachment. " + data + " " + attachmentId); - } - } - - if (MediaUtil.isImageType(contentType) || MediaUtil.isVideoType(contentType)) { - Glide.get(context).clearDiskCache(); - ThreadUtil.runOnMain(() -> Glide.get(context).clearMemory()); - } - } - - private @NonNull DataUsageResult getAttachmentFileUsages(@Nullable String data, @NonNull AttachmentId attachmentId) { - SQLiteDatabase database = databaseHelper.getSignalReadableDatabase(); - - if (!database.inTransaction()) { - throw new IllegalArgumentException("Must be in a transaction!"); - } - - if (data == null) return DataUsageResult.NOT_IN_USE; - - String selection = DATA + " = ? AND " + UNIQUE_ID + " != ? AND " + ROW_ID + " != ?"; - String[] args = {data, Long.toString(attachmentId.getUniqueId()), Long.toString(attachmentId.getRowId())}; - List quoteRows = new LinkedList<>(); - - try (Cursor cursor = database.query(TABLE_NAME, new String[]{ROW_ID, UNIQUE_ID, QUOTE}, selection, args, null, null, null, null)) { - while (cursor.moveToNext()) { - boolean isQuote = cursor.getInt(cursor.getColumnIndexOrThrow(QUOTE)) == 1; - if (isQuote) { - quoteRows.add(new AttachmentId(cursor.getLong(cursor.getColumnIndexOrThrow(ROW_ID)), cursor.getLong(cursor.getColumnIndexOrThrow(UNIQUE_ID)))); - } else { - return DataUsageResult.IN_USE; - } - } - } - - return new DataUsageResult(quoteRows); - } - - /** - * Check if data file is in use by another attachment row with a different hash. Rows with the same data and hash - * will be fixed in a later call to {@link #updateAttachmentAndMatchingHashes(SQLiteDatabase, AttachmentId, String, ContentValues)}. - */ - private boolean isAttachmentFileUsedByOtherAttachments(@Nullable AttachmentId attachmentId, @NonNull DataInfo dataInfo) { - if (attachmentId == null) { - return false; - } - - return SQLiteDatabaseExtensionsKt.exists(getReadableDatabase(), TABLE_NAME) - .where(DATA + " = ? AND " + DATA_HASH + " != ?", dataInfo.file.getAbsolutePath(), dataInfo.hash) - .run(); - } - - public void insertAttachmentsForPlaceholder(long mmsId, @NonNull AttachmentId attachmentId, @NonNull InputStream inputStream) - throws MmsException - { - DatabaseAttachment placeholder = getAttachment(attachmentId); - SQLiteDatabase database = databaseHelper.getSignalWritableDatabase(); - ContentValues values = new ContentValues(); - DataInfo oldInfo = getAttachmentDataFileInfo(attachmentId, DATA); - DataInfo dataInfo = storeAttachmentStream(inputStream); - File transferFile = getTransferFile(databaseHelper.getSignalReadableDatabase(), attachmentId); - boolean updated = false; - - database.beginTransaction(); - try { - dataInfo = deduplicateAttachment(dataInfo, attachmentId, placeholder != null ? placeholder.getTransformProperties() : TransformProperties.empty()); - if (oldInfo != null) { - updateAttachmentDataHash(database, oldInfo.hash, dataInfo); - } - - values.put(DATA, dataInfo.file.getAbsolutePath()); - values.put(SIZE, dataInfo.length); - values.put(DATA_RANDOM, dataInfo.random); - values.put(DATA_HASH, dataInfo.hash); - - String visualHashString = getVisualHashStringOrNull(placeholder); - if (visualHashString != null) { - values.put(VISUAL_HASH, visualHashString); - } - - values.put(TRANSFER_STATE, TRANSFER_PROGRESS_DONE); - values.put(TRANSFER_FILE, (String) null); - - values.put(TRANSFORM_PROPERTIES, TransformProperties.forSkipTransform().serialize()); - - updated = database.update(TABLE_NAME, values, PART_ID_WHERE, attachmentId.toStrings()) > 0; - - database.setTransactionSuccessful(); - } finally { - database.endTransaction(); - } - - if (updated) { - long threadId = SignalDatabase.messages().getThreadIdForMessage(mmsId); - - if (!SignalDatabase.messages().isStory(mmsId)) { - SignalDatabase.threads().updateSnippetUriSilently(threadId, PartAuthority.getAttachmentDataUri(attachmentId)); - } - - notifyConversationListeners(threadId); - notifyConversationListListeners(); - notifyAttachmentListeners(); - } else { - if (!dataInfo.file.delete()) { - Log.w(TAG, "Failed to delete unused attachment"); - } - } - - if (transferFile != null) { - if (!transferFile.delete()) { - Log.w(TAG, "Unable to delete transfer file."); - } - } - - if (placeholder != null && MediaUtil.isAudio(placeholder)) { - GenerateAudioWaveFormJob.enqueue(placeholder.getAttachmentId()); - } - } - - private static @Nullable String getVisualHashStringOrNull(@Nullable Attachment attachment) { - if (attachment == null) return null; - else if (attachment.getBlurHash() != null) return attachment.getBlurHash().getHash(); - else if (attachment.getAudioHash() != null) return attachment.getAudioHash().getHash(); - else return null; - } - - public void copyAttachmentData(@NonNull AttachmentId sourceId, @NonNull AttachmentId destinationId) - throws MmsException - { - DatabaseAttachment sourceAttachment = getAttachment(sourceId); - - if (sourceAttachment == null) { - throw new MmsException("Cannot find attachment for source!"); - } - - SQLiteDatabase database = databaseHelper.getSignalWritableDatabase(); - DataInfo sourceDataInfo = getAttachmentDataFileInfo(sourceId, DATA); - - if (sourceDataInfo == null) { - throw new MmsException("No attachment data found for source!"); - } - - ContentValues contentValues = new ContentValues(); - - contentValues.put(DATA, sourceDataInfo.file.getAbsolutePath()); - contentValues.put(DATA_HASH, sourceDataInfo.hash); - contentValues.put(SIZE, sourceDataInfo.length); - contentValues.put(DATA_RANDOM, sourceDataInfo.random); - - contentValues.put(TRANSFER_STATE, sourceAttachment.getTransferState()); - contentValues.put(CDN_NUMBER, sourceAttachment.getCdnNumber()); - contentValues.put(CONTENT_LOCATION, sourceAttachment.getLocation()); - contentValues.put(DIGEST, sourceAttachment.getDigest()); - contentValues.put(MAC_DIGEST, sourceAttachment.getIncrementalDigest()); - contentValues.put(INCREMENTAL_MAC_CHUNK_SIZE, sourceAttachment.getIncrementalMacChunkSize()); - contentValues.put(CONTENT_DISPOSITION, sourceAttachment.getKey()); - contentValues.put(NAME, sourceAttachment.getRelay()); - contentValues.put(SIZE, sourceAttachment.getSize()); - contentValues.put(FAST_PREFLIGHT_ID, sourceAttachment.getFastPreflightId()); - contentValues.put(WIDTH, sourceAttachment.getWidth()); - contentValues.put(HEIGHT, sourceAttachment.getHeight()); - contentValues.put(CONTENT_TYPE, sourceAttachment.getContentType()); - contentValues.put(VISUAL_HASH, getVisualHashStringOrNull(sourceAttachment)); - contentValues.put(TRANSFORM_PROPERTIES, sourceAttachment.getTransformProperties().serialize()); - - database.update(TABLE_NAME, contentValues, PART_ID_WHERE, destinationId.toStrings()); - } - - public void updateAttachmentCaption(@NonNull AttachmentId id, @Nullable String caption) { - ContentValues values = new ContentValues(1); - values.put(CAPTION, caption); - - databaseHelper.getSignalWritableDatabase().update(TABLE_NAME, values, PART_ID_WHERE, id.toStrings()); - } - - public void updateDisplayOrder(@NonNull Map orderMap) { - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - - db.beginTransaction(); - try { - for (Map.Entry entry : orderMap.entrySet()) { - ContentValues values = new ContentValues(1); - values.put(DISPLAY_ORDER, entry.getValue()); - - databaseHelper.getSignalWritableDatabase().update(TABLE_NAME, values, PART_ID_WHERE, entry.getKey().toStrings()); - } - - db.setTransactionSuccessful(); - } finally { - db.endTransaction(); - } - - } - - public void updateAttachmentAfterUpload(@NonNull AttachmentId id, @NonNull Attachment attachment, long uploadTimestamp) { - SQLiteDatabase database = databaseHelper.getSignalWritableDatabase(); - DataInfo dataInfo = getAttachmentDataFileInfo(id, DATA); - ContentValues values = new ContentValues(); - - values.put(TRANSFER_STATE, TRANSFER_PROGRESS_DONE); - values.put(CDN_NUMBER, attachment.getCdnNumber()); - values.put(CONTENT_LOCATION, attachment.getLocation()); - values.put(DIGEST, attachment.getDigest()); - values.put(MAC_DIGEST, attachment.getIncrementalDigest()); - values.put(INCREMENTAL_MAC_CHUNK_SIZE, attachment.getIncrementalMacChunkSize()); - values.put(CONTENT_DISPOSITION, attachment.getKey()); - values.put(NAME, attachment.getRelay()); - values.put(SIZE, attachment.getSize()); - values.put(FAST_PREFLIGHT_ID, attachment.getFastPreflightId()); - values.put(VISUAL_HASH, getVisualHashStringOrNull(attachment)); - values.put(UPLOAD_TIMESTAMP, uploadTimestamp); - - if (dataInfo != null && dataInfo.hash != null) { - updateAttachmentAndMatchingHashes(database, id, dataInfo.hash, values); - } else { - database.update(TABLE_NAME, values, PART_ID_WHERE, id.toStrings()); - } - } - - public @NonNull DatabaseAttachment insertAttachmentForPreUpload(@NonNull Attachment attachment) throws MmsException { - Map result = insertAttachmentsForMessage(PREUPLOAD_MESSAGE_ID, - Collections.singletonList(attachment), - Collections.emptyList()); - - if (result.values().isEmpty()) { - throw new MmsException("Bad attachment result!"); - } - - DatabaseAttachment databaseAttachment = getAttachment(result.values().iterator().next()); - - if (databaseAttachment == null) { - throw new MmsException("Failed to retrieve attachment we just inserted!"); - } - - return databaseAttachment; - } - - public void updateMessageId(@NonNull Collection attachmentIds, long mmsId, boolean isStory) { - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - - db.beginTransaction(); - try { - ContentValues values = new ContentValues(1); - values.put(MMS_ID, mmsId); - - if (!isStory) { - values.putNull(CAPTION); - } - - int updatedCount = 0; - int attachmentIdSize = 0; - for (AttachmentId attachmentId : attachmentIds) { - attachmentIdSize++; - updatedCount = db.update(TABLE_NAME, values, PART_ID_WHERE, attachmentId.toStrings()); - } - - Log.d(TAG, "[updateMessageId] Updated " + updatedCount + " out of " + attachmentIdSize + " ids."); - - db.setTransactionSuccessful(); - } finally { - db.endTransaction(); - } - } - - @NonNull Map insertAttachmentsForMessage(long mmsId, @NonNull List attachments, @NonNull List quoteAttachment) - throws MmsException - { - if (attachments.isEmpty() && quoteAttachment.isEmpty()) { - return Collections.emptyMap(); - } - - Log.d(TAG, "insertParts(" + attachments.size() + ")"); - - Map insertedAttachments = new HashMap<>(); - - for (Attachment attachment : attachments) { - AttachmentId attachmentId = insertAttachment(mmsId, attachment, attachment.isQuote()); - insertedAttachments.put(attachment, attachmentId); - Log.i(TAG, "Inserted attachment at ID: " + attachmentId); - } - - try { - for (Attachment attachment : quoteAttachment) { - AttachmentId attachmentId = insertAttachment(mmsId, attachment, true); - insertedAttachments.put(attachment, attachmentId); - Log.i(TAG, "Inserted quoted attachment at ID: " + attachmentId); - } - } catch (MmsException e) { - Log.w(TAG, "Failed to insert quote attachment! messageId: " + mmsId); - } - - return insertedAttachments; - } - - /** - * @param onlyModifyThisAttachment If false and more than one attachment shares this file and quality, they will all - * be updated. If true, then guarantees not to affect other attachments. - */ - public void updateAttachmentData(@NonNull DatabaseAttachment databaseAttachment, - @NonNull MediaStream mediaStream, - boolean onlyModifyThisAttachment) - throws MmsException, IOException - { - SQLiteDatabase database = databaseHelper.getSignalWritableDatabase(); - DataInfo oldDataInfo = getAttachmentDataFileInfo(databaseAttachment.getAttachmentId(), DATA); - - if (oldDataInfo == null) { - throw new MmsException("No attachment data found!"); - } - - File destination = oldDataInfo.file; - - boolean isSingleUseOfData = onlyModifyThisAttachment || oldDataInfo.hash == null; - if (isSingleUseOfData) { - if (fileReferencedByMoreThanOneAttachment(destination)) { - Log.i(TAG, "Creating a new file as this one is used by more than one attachment"); - destination = newFile(); - } - } - - DataInfo dataInfo = storeAttachmentStream(destination, mediaStream.getStream()); - - database.beginTransaction(); - try { - dataInfo = deduplicateAttachment(dataInfo, databaseAttachment.getAttachmentId(), databaseAttachment.getTransformProperties()); - - ContentValues contentValues = new ContentValues(); - contentValues.put(SIZE, dataInfo.length); - contentValues.put(CONTENT_TYPE, mediaStream.getMimeType()); - contentValues.put(WIDTH, mediaStream.getWidth()); - contentValues.put(HEIGHT, mediaStream.getHeight()); - contentValues.put(DATA, dataInfo.file.getAbsolutePath()); - contentValues.put(DATA_RANDOM, dataInfo.random); - contentValues.put(DATA_HASH, dataInfo.hash); - - int updateCount = updateAttachmentAndMatchingHashes(database, - databaseAttachment.getAttachmentId(), - isSingleUseOfData ? dataInfo.hash : oldDataInfo.hash, - contentValues); - - Log.i(TAG, "[updateAttachmentData] Updated " + updateCount + " rows."); - - database.setTransactionSuccessful(); - } finally { - database.endTransaction(); - } - } - - /** - * Returns true if the file referenced by two or more attachments. - * Returns false if the file is referenced by zero or one attachments. - */ - private boolean fileReferencedByMoreThanOneAttachment(@NonNull File file) { - SQLiteDatabase database = databaseHelper.getSignalReadableDatabase(); - String selection = DATA + " = ?"; - String[] args = new String[]{file.getAbsolutePath()}; - - try (Cursor cursor = database.query(TABLE_NAME, null, selection, args, null, null, null, "2")) { - return cursor != null && cursor.moveToFirst() && cursor.moveToNext(); - } - } - - public void markAttachmentAsTransformed(@NonNull AttachmentId attachmentId, boolean withFaststart) { - getWritableDatabase().beginTransaction(); - try { - TransformProperties transformProperties = getTransformProperties(attachmentId); - - if (transformProperties == null) { - Log.w(TAG, "Failed to get transformation properties, attachment no longer exists."); - return; - } - - transformProperties = transformProperties.withSkipTransform(); - - if (withFaststart) { - transformProperties = transformProperties.withMp4Faststart(); - } - - updateAttachmentTransformProperties(attachmentId, transformProperties); - getWritableDatabase().setTransactionSuccessful(); - } catch (Exception e) { - Log.w(TAG, "Could not mark attachment as transformed.", e); - } finally { - getWritableDatabase().endTransaction(); - } - } - - /** - * @return null if we fail to find the given attachmentId - */ - public @Nullable TransformProperties getTransformProperties(@NonNull AttachmentId attachmentId) { - String[] projection = SqlUtil.buildArgs(TRANSFORM_PROPERTIES); - String[] args = attachmentId.toStrings(); - - try (Cursor cursor = getWritableDatabase().query(TABLE_NAME, projection, PART_ID_WHERE, args, null, null, null, null)) { - if (cursor.moveToFirst()) { - String serializedProperties = CursorUtil.requireString(cursor, TRANSFORM_PROPERTIES); - return TransformProperties.parse(serializedProperties); - } else { - return null; - } - } - } - - private void updateAttachmentTransformProperties(@NonNull AttachmentId attachmentId, @NonNull TransformProperties transformProperties) { - DataInfo dataInfo = getAttachmentDataFileInfo(attachmentId, DATA); - - if (dataInfo == null) { - Log.w(TAG, "[updateAttachmentTransformProperties] No data info found!"); - return; - } - - ContentValues contentValues = new ContentValues(); - contentValues.put(TRANSFORM_PROPERTIES, transformProperties.serialize()); - - int updateCount = updateAttachmentAndMatchingHashes(databaseHelper.getSignalWritableDatabase(), attachmentId, dataInfo.hash, contentValues); - Log.i(TAG, "[updateAttachmentTransformProperties] Updated " + updateCount + " rows."); - } - - public @NonNull File getOrCreateTransferFile(@NonNull AttachmentId attachmentId) throws IOException { - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - File existing = getTransferFile(db, attachmentId); - - if (existing != null) { - return existing; - } - - File transferFile = newTransferFile(); - ContentValues values = new ContentValues(); - values.put(TRANSFER_FILE, transferFile.getAbsolutePath()); - - db.update(TABLE_NAME, values, PART_ID_WHERE, attachmentId.toStrings()); - - return transferFile; - } - - private @Nullable static File getTransferFile(@NonNull SQLiteDatabase db, @NonNull AttachmentId attachmentId) { - try (Cursor cursor = db.query(TABLE_NAME, new String[] { TRANSFER_FILE }, PART_ID_WHERE, attachmentId.toStrings(), null, null, "1")) { - if (cursor != null && cursor.moveToFirst()) { - String path = cursor.getString(cursor.getColumnIndexOrThrow(TRANSFER_FILE)); - if (path != null) { - return new File(path); - } - } - } - - return null; - } - - private static int updateAttachmentAndMatchingHashes(@NonNull SQLiteDatabase database, - @NonNull AttachmentId attachmentId, - @Nullable String dataHash, - @NonNull ContentValues contentValues) - { - String selection = "(" + ROW_ID + " = ? AND " + UNIQUE_ID + " = ?) OR " + - "(" + DATA_HASH + " NOT NULL AND " + DATA_HASH + " = ?)"; - String[] args = new String[]{String.valueOf(attachmentId.getRowId()), - String.valueOf(attachmentId.getUniqueId()), - String.valueOf(dataHash)}; - - return database.update(TABLE_NAME, contentValues, selection, args); - } - - private static void updateAttachmentDataHash(@NonNull SQLiteDatabase database, - @NonNull String oldHash, - @NonNull DataInfo newData) - { - if (oldHash == null) return; - - ContentValues contentValues = new ContentValues(); - contentValues.put(DATA, newData.file.getAbsolutePath()); - contentValues.put(DATA_RANDOM, newData.random); - contentValues.put(DATA_HASH, newData.hash); - database.update(TABLE_NAME, - contentValues, - DATA_HASH + " = ?", - new String[]{oldHash}); - } - - public void updateAttachmentFileName(@NonNull AttachmentId attachmentId, - @Nullable String fileName) - { - SQLiteDatabase database = databaseHelper.getSignalWritableDatabase(); - - ContentValues contentValues = new ContentValues(1); - contentValues.put(FILE_NAME, StorageUtil.getCleanFileName(fileName)); - - database.update(TABLE_NAME, contentValues, PART_ID_WHERE, attachmentId.toStrings()); - } - - public void markAttachmentUploaded(long messageId, Attachment attachment) { - ContentValues values = new ContentValues(1); - SQLiteDatabase database = databaseHelper.getSignalWritableDatabase(); - - values.put(TRANSFER_STATE, TRANSFER_PROGRESS_DONE); - database.update(TABLE_NAME, values, PART_ID_WHERE, ((DatabaseAttachment)attachment).getAttachmentId().toStrings()); - - notifyConversationListeners(SignalDatabase.messages().getThreadIdForMessage(messageId)); - } - - public void setTransferState(long messageId, @NonNull Attachment attachment, int transferState) { - if (!(attachment instanceof DatabaseAttachment)) { - throw new AssertionError("Attempt to update attachment that doesn't belong to DB!"); - } - - setTransferState(messageId, ((DatabaseAttachment) attachment).getAttachmentId(), transferState); - } - - public void setTransferState(long messageId, @NonNull AttachmentId attachmentId, int transferState) { - final ContentValues values = new ContentValues(1); - final SQLiteDatabase database = databaseHelper.getSignalWritableDatabase(); - - values.put(TRANSFER_STATE, transferState); - database.update(TABLE_NAME, values, PART_ID_WHERE, attachmentId.toStrings()); - notifyConversationListeners(SignalDatabase.messages().getThreadIdForMessage(messageId)); - } - - /** - * Returns (pack_id, pack_key) pairs that are referenced in attachments but not in the stickers - * database. - */ - public @Nullable Cursor getUnavailableStickerPacks() { - String query = "SELECT DISTINCT " + STICKER_PACK_ID + ", " + STICKER_PACK_KEY + - " FROM " + TABLE_NAME + - " WHERE " + - STICKER_PACK_ID + " NOT NULL AND " + - STICKER_PACK_KEY + " NOT NULL AND " + - STICKER_PACK_ID + " NOT IN (" + - "SELECT DISTINCT " + StickerTable.PACK_ID + " FROM " + StickerTable.TABLE_NAME + - ")"; - - return databaseHelper.getSignalReadableDatabase().rawQuery(query, null); - } - - public boolean hasStickerAttachments() { - String selection = STICKER_PACK_ID + " NOT NULL"; - - try (Cursor cursor = databaseHelper.getSignalReadableDatabase().query(TABLE_NAME, null, selection, null, null, null, null, "1")) { - return cursor != null && cursor.moveToFirst(); - } - } - - @SuppressWarnings("WeakerAccess") - private @Nullable InputStream getDataStream(AttachmentId attachmentId, String dataType, long offset) - throws FileNotFoundException - { - DataInfo dataInfo = getAttachmentDataFileInfo(attachmentId, dataType); - - if (dataInfo == null) { - return null; - } - - try { - if (dataInfo.random != null && dataInfo.random.length == 32) { - return ModernDecryptingPartInputStream.createFor(attachmentSecret, dataInfo.random, dataInfo.file, offset); - } else { - InputStream stream = ClassicDecryptingPartInputStream.createFor(attachmentSecret, dataInfo.file); - long skipped = stream.skip(offset); - - if (skipped != offset) { - Log.w(TAG, "Skip failed: " + skipped + " vs " + offset); - return null; - } - - return stream; - } - } catch (FileNotFoundException e) { - Log.w(TAG, e); - throw e; - } catch (IOException e) { - Log.w(TAG, e); - return null; - } - } - - @VisibleForTesting - @Nullable DataInfo getAttachmentDataFileInfo(@NonNull AttachmentId attachmentId, @NonNull String dataType) - { - SQLiteDatabase database = databaseHelper.getSignalReadableDatabase(); - - try (Cursor cursor = database.query(TABLE_NAME, new String[] { dataType, SIZE, DATA_RANDOM, DATA_HASH, TRANSFORM_PROPERTIES }, PART_ID_WHERE, attachmentId.toStrings(), null, null, null)) { - if (cursor != null && cursor.moveToFirst()) { - if (cursor.isNull(cursor.getColumnIndexOrThrow(dataType))) { - return null; - } - - return new DataInfo(new File(cursor.getString(cursor.getColumnIndexOrThrow(dataType))), - cursor.getLong(cursor.getColumnIndexOrThrow(SIZE)), - cursor.getBlob(cursor.getColumnIndexOrThrow(DATA_RANDOM)), - cursor.getString(cursor.getColumnIndexOrThrow(DATA_HASH)), - TransformProperties.parse(CursorUtil.requireString(cursor, TRANSFORM_PROPERTIES))); - } else { - return null; - } - } - - } - - private @NonNull DataInfo storeAttachmentStream(@NonNull InputStream in) throws MmsException { - try { - return storeAttachmentStream(newFile(), in); - } catch (IOException e) { - throw new MmsException(e); - } - } - - public File newFile() throws IOException { - return newFile(context); - } - - private File newTransferFile() throws IOException { - File partsDirectory = context.getDir(DIRECTORY, Context.MODE_PRIVATE); - return PartFileProtector.protect(() -> File.createTempFile("transfer", ".mms", partsDirectory)); - } - - public static File newFile(Context context) throws IOException { - File partsDirectory = context.getDir(DIRECTORY, Context.MODE_PRIVATE); - return PartFileProtector.protect(() -> File.createTempFile("part", ".mms", partsDirectory)); - } - - /** - * Reads the entire stream and saves to disk. If you need to deduplicate attachments, call {@link #deduplicateAttachment(DataInfo, AttachmentId, TransformProperties)} - * afterwards and use the {@link DataInfo} returned by it instead. - */ - private @NonNull DataInfo storeAttachmentStream(@NonNull File destination, @NonNull InputStream in) throws MmsException { - try { - File tempFile = newFile(); - MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); - DigestInputStream digestInputStream = new DigestInputStream(in, messageDigest); - Pair out = ModernEncryptingPartOutputStream.createFor(attachmentSecret, tempFile, false); - long length = StreamUtil.copy(digestInputStream, out.second); - String hash = Base64.encodeWithPadding(digestInputStream.getMessageDigest().digest()); - - if (!tempFile.renameTo(destination)) { - Log.w(TAG, "Couldn't rename " + tempFile.getPath() + " to " + destination.getPath()); - tempFile.delete(); - throw new IllegalStateException("Couldn't rename " + tempFile.getPath() + " to " + destination.getPath()); - } - - return new DataInfo(destination, length, out.first, hash, null); - } catch (IOException | NoSuchAlgorithmException e) { - throw new MmsException(e); - } - } - - private @NonNull DataInfo deduplicateAttachment(@NonNull DataInfo dataInfo, - @Nullable AttachmentId attachmentId, - @NonNull TransformProperties transformProperties) - { - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - - if (!db.inTransaction()) { - throw new IllegalStateException("Must be in a transaction!"); - } - - List sharedDataInfos = findDuplicateDataFileInfos(db, dataInfo.hash, attachmentId); - for (DataInfo sharedDataInfo : sharedDataInfos) { - if (dataInfo.file.equals(sharedDataInfo.file)) { - continue; - } - - boolean isUsedElsewhere = isAttachmentFileUsedByOtherAttachments(attachmentId, dataInfo); - boolean isSameQuality = transformProperties.sentMediaQuality == sharedDataInfo.transformProperties.sentMediaQuality; - - Log.i(TAG, "[deduplicateAttachment] Potential duplicate data file found. usedElsewhere: " + isUsedElsewhere + " sameQuality: " + isSameQuality + " otherFile: " + sharedDataInfo.file.getAbsolutePath()); - - if (!isSameQuality) { - continue; - } - - if (!isUsedElsewhere) { - if (dataInfo.file.delete()) { - Log.i(TAG, "[deduplicateAttachment] Deleted original file. " + dataInfo.file); - } else { - Log.w(TAG, "[deduplicateAttachment] Original file could not be deleted."); - } - } - - return sharedDataInfo; - } - - Log.i(TAG, "[deduplicateAttachment] No acceptable matching attachment data found. " + dataInfo.file.getAbsolutePath()); - return dataInfo; - } - - private static @NonNull List findDuplicateDataFileInfos(@NonNull SQLiteDatabase database, - @NonNull String hash, - @Nullable AttachmentId excludedAttachmentId) - { - if (!database.inTransaction()) { - throw new IllegalArgumentException("Must be in a transaction!"); - } - - Pair selectorArgs = buildSharedFileSelectorArgs(hash, excludedAttachmentId); - return CursorExtensionsKt.readToList(database.query(TABLE_NAME, - new String[] { DATA, DATA_RANDOM, SIZE, TRANSFORM_PROPERTIES }, - selectorArgs.first, - selectorArgs.second, - null, - null, - null, - null), - cursor -> new DataInfo(new File(CursorUtil.requireString(cursor, DATA)), - CursorUtil.requireLong(cursor, SIZE), - CursorUtil.requireBlob(cursor, DATA_RANDOM), - hash, - TransformProperties.parse(CursorUtil.requireString(cursor, TRANSFORM_PROPERTIES)))); - } - - private static Pair buildSharedFileSelectorArgs(@NonNull String newHash, - @Nullable AttachmentId attachmentId) - { - final String selector; - final String[] selection; - - if (attachmentId == null) { - selector = DATA_HASH + " = ?"; - selection = new String[]{newHash}; - } else { - selector = PART_ID_WHERE_NOT + " AND " + DATA_HASH + " = ?"; - selection = new String[]{Long.toString(attachmentId.getRowId()), - Long.toString(attachmentId.getUniqueId()), - newHash}; - } - - return Pair.create(selector, selection); - } - - public List getAttachments(@NonNull Cursor cursor) { - try { - if (cursor.getColumnIndex(AttachmentTable.ATTACHMENT_JSON_ALIAS) != -1) { - if (cursor.isNull(cursor.getColumnIndexOrThrow(ATTACHMENT_JSON_ALIAS))) { - return new LinkedList<>(); - } - - List result = new LinkedList<>(); - JSONArray array = new JSONArray(cursor.getString(cursor.getColumnIndexOrThrow(ATTACHMENT_JSON_ALIAS))); - - for (int i=0;i= 0 - ? new StickerLocator(object.getString(STICKER_PACK_ID), - object.getString(STICKER_PACK_KEY), - object.getInt(STICKER_ID), - object.getString(STICKER_EMOJI)) - : null, - MediaUtil.isAudioType(contentType) ? null : BlurHash.parseOrNull(object.getString(VISUAL_HASH)), - MediaUtil.isAudioType(contentType) ? AudioHash.parseOrNull(object.getString(VISUAL_HASH)) : null, - TransformProperties.parse(object.getString(TRANSFORM_PROPERTIES)), - object.getInt(DISPLAY_ORDER), - object.getLong(UPLOAD_TIMESTAMP))); - } - } - - return result; - } else { - return Collections.singletonList(getAttachment(cursor)); - } - } catch (JSONException e) { - throw new AssertionError(e); - } - } - - private @NonNull DatabaseAttachment getAttachment(@NonNull Cursor cursor) { - String contentType = cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_TYPE)); - return new DatabaseAttachment(new AttachmentId(cursor.getLong(cursor.getColumnIndexOrThrow(ROW_ID)), - cursor.getLong(cursor.getColumnIndexOrThrow(UNIQUE_ID))), - cursor.getLong(cursor.getColumnIndexOrThrow(MMS_ID)), - !cursor.isNull(cursor.getColumnIndexOrThrow(DATA)), - MediaUtil.isImageType(contentType) || MediaUtil.isVideoType(contentType), - contentType, - cursor.getInt(cursor.getColumnIndexOrThrow(TRANSFER_STATE)), - cursor.getLong(cursor.getColumnIndexOrThrow(SIZE)), - cursor.getString(cursor.getColumnIndexOrThrow(FILE_NAME)), - cursor.getInt(cursor.getColumnIndexOrThrow(CDN_NUMBER)), - cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_LOCATION)), - cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_DISPOSITION)), - cursor.getString(cursor.getColumnIndexOrThrow(NAME)), - cursor.getBlob(cursor.getColumnIndexOrThrow(DIGEST)), - cursor.getBlob(cursor.getColumnIndexOrThrow(MAC_DIGEST)), - cursor.getInt(cursor.getColumnIndexOrThrow(INCREMENTAL_MAC_CHUNK_SIZE)), - cursor.getString(cursor.getColumnIndexOrThrow(FAST_PREFLIGHT_ID)), - cursor.getInt(cursor.getColumnIndexOrThrow(VOICE_NOTE)) == 1, - cursor.getInt(cursor.getColumnIndexOrThrow(BORDERLESS)) == 1, - cursor.getInt(cursor.getColumnIndexOrThrow(VIDEO_GIF)) == 1, - cursor.getInt(cursor.getColumnIndexOrThrow(WIDTH)), - cursor.getInt(cursor.getColumnIndexOrThrow(HEIGHT)), - cursor.getInt(cursor.getColumnIndexOrThrow(QUOTE)) == 1, - cursor.getString(cursor.getColumnIndexOrThrow(CAPTION)), - cursor.getInt(cursor.getColumnIndexOrThrow(STICKER_ID)) >= 0 - ? new StickerLocator(CursorUtil.requireString(cursor, STICKER_PACK_ID), - CursorUtil.requireString(cursor, STICKER_PACK_KEY), - CursorUtil.requireInt(cursor, STICKER_ID), - CursorUtil.requireString(cursor, STICKER_EMOJI)) - : null, - MediaUtil.isAudioType(contentType) ? null : BlurHash.parseOrNull(cursor.getString(cursor.getColumnIndexOrThrow(VISUAL_HASH))), - MediaUtil.isAudioType(contentType) ? AudioHash.parseOrNull(cursor.getString(cursor.getColumnIndexOrThrow(VISUAL_HASH))) : null, - TransformProperties.parse(cursor.getString(cursor.getColumnIndexOrThrow(TRANSFORM_PROPERTIES))), - cursor.getInt(cursor.getColumnIndexOrThrow(DISPLAY_ORDER)), - cursor.getLong(cursor.getColumnIndexOrThrow(UPLOAD_TIMESTAMP))); - } - - private AttachmentId insertAttachment(long mmsId, Attachment attachment, boolean quote) - throws MmsException - { - Log.d(TAG, "Inserting attachment for mms id: " + mmsId); - SQLiteDatabase database = databaseHelper.getSignalWritableDatabase(); - - AttachmentId attachmentId = null; - boolean notifyPacks = false; - - database.beginTransaction(); - try { - DataInfo dataInfo = null; - long uniqueId = System.currentTimeMillis(); - - if (attachment.getUri() != null) { - DataInfo storeDataInfo = storeAttachmentStream(PartAuthority.getAttachmentStream(context, attachment.getUri())); - Log.d(TAG, "Wrote part to file: " + storeDataInfo.file.getAbsolutePath()); - dataInfo = deduplicateAttachment(storeDataInfo, attachmentId, attachment.getTransformProperties()); - } - - Attachment template = attachment; - boolean useTemplateUpload = false; - - if (dataInfo != null && dataInfo.hash != null) { - List possibleTemplates = findTemplateAttachments(dataInfo.hash); - - for (Attachment possibleTemplate : possibleTemplates) { - useTemplateUpload = possibleTemplate.getUploadTimestamp() > attachment.getUploadTimestamp() && - possibleTemplate.getTransferState() == TRANSFER_PROGRESS_DONE && - possibleTemplate.getTransformProperties().shouldSkipTransform() && - possibleTemplate.getDigest() != null && - !attachment.getTransformProperties().isVideoEdited() && - possibleTemplate.getTransformProperties().sentMediaQuality == attachment.getTransformProperties().getSentMediaQuality(); - - if (useTemplateUpload) { - Log.i(TAG, "Found a duplicate attachment upon insertion. Using it as a template."); - template = possibleTemplate; - break; - } - } - } - - ContentValues contentValues = new ContentValues(); - contentValues.put(MMS_ID, mmsId); - contentValues.put(CONTENT_TYPE, template.getContentType()); - contentValues.put(TRANSFER_STATE, attachment.getTransferState()); - contentValues.put(UNIQUE_ID, uniqueId); - contentValues.put(CDN_NUMBER, useTemplateUpload ? template.getCdnNumber() : attachment.getCdnNumber()); - contentValues.put(CONTENT_LOCATION, useTemplateUpload ? template.getLocation() : attachment.getLocation()); - contentValues.put(DIGEST, useTemplateUpload ? template.getDigest() : attachment.getDigest()); - contentValues.put(MAC_DIGEST, useTemplateUpload ? template.getIncrementalDigest() : attachment.getIncrementalDigest()); - contentValues.put(INCREMENTAL_MAC_CHUNK_SIZE, useTemplateUpload ? template.getIncrementalMacChunkSize() : attachment.getIncrementalMacChunkSize()); - contentValues.put(CONTENT_DISPOSITION, useTemplateUpload ? template.getKey() : attachment.getKey()); - contentValues.put(NAME, useTemplateUpload ? template.getRelay() : attachment.getRelay()); - contentValues.put(FILE_NAME, StorageUtil.getCleanFileName(attachment.getFileName())); - contentValues.put(SIZE, template.getSize()); - contentValues.put(FAST_PREFLIGHT_ID, attachment.getFastPreflightId()); - contentValues.put(VOICE_NOTE, attachment.isVoiceNote() ? 1 : 0); - contentValues.put(BORDERLESS, attachment.isBorderless() ? 1 : 0); - contentValues.put(VIDEO_GIF, attachment.isVideoGif() ? 1 : 0); - contentValues.put(WIDTH, template.getWidth()); - contentValues.put(HEIGHT, template.getHeight()); - contentValues.put(QUOTE, quote); - contentValues.put(CAPTION, attachment.getCaption()); - contentValues.put(UPLOAD_TIMESTAMP, useTemplateUpload ? template.getUploadTimestamp() : attachment.getUploadTimestamp()); - if (attachment.getTransformProperties().isVideoEdited()) { - contentValues.putNull(VISUAL_HASH); - contentValues.put(TRANSFORM_PROPERTIES, attachment.getTransformProperties().serialize()); - } else { - contentValues.put(VISUAL_HASH, getVisualHashStringOrNull(template)); - contentValues.put(TRANSFORM_PROPERTIES, (useTemplateUpload ? template : attachment).getTransformProperties().serialize()); - } - - if (attachment.isSticker()) { - contentValues.put(STICKER_PACK_ID, attachment.getSticker().getPackId()); - contentValues.put(STICKER_PACK_KEY, attachment.getSticker().getPackKey()); - contentValues.put(STICKER_ID, attachment.getSticker().getStickerId()); - contentValues.put(STICKER_EMOJI, attachment.getSticker().getEmoji()); - } - - if (dataInfo != null) { - contentValues.put(DATA, dataInfo.file.getAbsolutePath()); - contentValues.put(SIZE, dataInfo.length); - contentValues.put(DATA_RANDOM, dataInfo.random); - if (attachment.getTransformProperties().isVideoEdited()) { - contentValues.putNull(DATA_HASH); - } else { - contentValues.put(DATA_HASH, dataInfo.hash); - } - } - - long rowId = database.insert(TABLE_NAME, null, contentValues); - - attachmentId = new AttachmentId(rowId, uniqueId); - notifyPacks = attachment.isSticker() && !hasStickerAttachments(); - - database.setTransactionSuccessful(); - } catch (IOException e) { - throw new MmsException(e); - } finally { - database.endTransaction(); - } - - if (notifyPacks) { - notifyStickerPackListeners(); - } - - notifyAttachmentListeners(); - - return attachmentId; - } - - private @NonNull List findTemplateAttachments(@NonNull String dataHash) { - String selection = DATA_HASH + " = ?"; - String[] args = new String[] { dataHash }; - - return CursorExtensionsKt.readToList(databaseHelper.getSignalReadableDatabase().query(TABLE_NAME, null, selection, args, null, null, null), this::getAttachment); - } - - @WorkerThread - public void writeAudioHash(@NonNull AttachmentId attachmentId, @Nullable AudioWaveFormData audioWaveForm) { - Log.i(TAG, "updating part audio wave form for #" + attachmentId); - - SQLiteDatabase database = databaseHelper.getSignalWritableDatabase(); - ContentValues values = new ContentValues(1); - - if (audioWaveForm != null) { - values.put(VISUAL_HASH, new AudioHash(audioWaveForm).getHash()); - } else { - values.putNull(VISUAL_HASH); - } - - database.update(TABLE_NAME, values, PART_ID_WHERE, attachmentId.toStrings()); - } - - - @RequiresApi(23) - public @Nullable MediaDataSource mediaDataSourceFor(@NonNull AttachmentId attachmentId, Boolean allowReadingFromTempFile) { - DataInfo dataInfo = getAttachmentDataFileInfo(attachmentId, DATA); - - if (dataInfo != null) { - return EncryptedMediaDataSource.createFor(attachmentSecret, dataInfo.file, dataInfo.random, dataInfo.length); - } - - if (allowReadingFromTempFile) { - Log.d(TAG, "Completed data file not found for video attachment, checking for in-progress files."); - - SQLiteDatabase database = databaseHelper.getSignalReadableDatabase(); - - File transferFile = getTransferFile(database, attachmentId); - - if (transferFile != null) { - return EncryptedMediaDataSource.createForDiskBlob(attachmentSecret, transferFile); - } - } - - Log.w(TAG, "No data file found for video attachment!"); - - return null; - } - - public void duplicateAttachmentsForMessage(long destinationMessageId, long sourceMessageId, Collection excludedIds) { - SQLiteDatabaseExtensionsKt.withinTransaction(getWritableDatabase(), db -> { - db.execSQL("CREATE TEMPORARY TABLE tmp_part AS SELECT * FROM " + TABLE_NAME + " WHERE " + MMS_ID + " = ?", SqlUtil.buildArgs(sourceMessageId)); - List queries = SqlUtil.buildCollectionQuery(ROW_ID, excludedIds); - for (SqlUtil.Query query : queries) { - db.delete("tmp_part", query.getWhere(), query.getWhereArgs()); - } - db.execSQL("UPDATE tmp_part SET " + ROW_ID + " = NULL, " + MMS_ID + " = ?", SqlUtil.buildArgs(destinationMessageId)); - db.execSQL("INSERT INTO " + TABLE_NAME + " SELECT * FROM tmp_part"); - db.execSQL("DROP TABLE tmp_part"); - return 0; - }); - } - - @VisibleForTesting - static class DataInfo { - final File file; - final long length; - final byte[] random; - final String hash; - final TransformProperties transformProperties; - - private DataInfo(File file, long length, byte[] random, String hash, TransformProperties transformProperties) { - this.file = file; - this.length = length; - this.random = random; - this.hash = hash; - this.transformProperties = transformProperties; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - final DataInfo dataInfo = (DataInfo) o; - return length == dataInfo.length && - Objects.equals(file, dataInfo.file) && - Arrays.equals(random, dataInfo.random) && - Objects.equals(hash, dataInfo.hash) && - Objects.equals(transformProperties, dataInfo.transformProperties); - } - - @Override - public int hashCode() { - int result = Objects.hash(file, length, hash, transformProperties); - result = 31 * result + Arrays.hashCode(random); - return result; - } - } - - private static final class DataUsageResult { - private final boolean hasStrongReference; - private final List removableWeakReferences; - - private static final DataUsageResult IN_USE = new DataUsageResult(true, Collections.emptyList()); - private static final DataUsageResult NOT_IN_USE = new DataUsageResult(false, Collections.emptyList()); - - DataUsageResult(@NonNull List removableWeakReferences) { - this(false, removableWeakReferences); - } - - private DataUsageResult(boolean hasStrongReference, @NonNull List removableWeakReferences) { - if (hasStrongReference && removableWeakReferences.size() > 0) { - throw new AssertionError(); - } - this.hasStrongReference = hasStrongReference; - this.removableWeakReferences = removableWeakReferences; - } - - boolean hasStrongReference() { - return hasStrongReference; - } - - /** - * Entries in here can be removed from the database. - *

- * Only possible to be non-empty when {@link #hasStrongReference} is false. - */ - @NonNull List getRemovableWeakReferences() { - return removableWeakReferences; - } - } - - public static final class TransformProperties implements Parcelable { - - private static final int DEFAULT_MEDIA_QUALITY = SentMediaQuality.STANDARD.getCode(); - - @JsonProperty private final boolean skipTransform; - @JsonProperty private final boolean videoTrim; - @JsonProperty private final long videoTrimStartTimeUs; - @JsonProperty private final long videoTrimEndTimeUs; - @JsonProperty private final int sentMediaQuality; - @JsonProperty private final boolean mp4Faststart; - - @JsonCreator - public TransformProperties(@JsonProperty("skipTransform") boolean skipTransform, - @JsonProperty("videoTrim") boolean videoTrim, - @JsonProperty("videoTrimStartTimeUs") long videoTrimStartTimeUs, - @JsonProperty("videoTrimEndTimeUs") long videoTrimEndTimeUs, - @JsonProperty("sentMediaQuality") int sentMediaQuality, - @JsonProperty("mp4Faststart") boolean mp4Faststart) - { - this.skipTransform = skipTransform; - this.videoTrim = videoTrim; - this.videoTrimStartTimeUs = videoTrimStartTimeUs; - this.videoTrimEndTimeUs = videoTrimEndTimeUs; - this.sentMediaQuality = sentMediaQuality; - this.mp4Faststart = mp4Faststart; - } - - protected TransformProperties(Parcel in) { - skipTransform = in.readByte() != 0; - videoTrim = in.readByte() != 0; - videoTrimStartTimeUs = in.readLong(); - videoTrimEndTimeUs = in.readLong(); - sentMediaQuality = in.readInt(); - mp4Faststart = in.readByte() != 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeByte((byte) (skipTransform ? 1 : 0)); - dest.writeByte((byte) (videoTrim ? 1 : 0)); - dest.writeLong(videoTrimStartTimeUs); - dest.writeLong(videoTrimEndTimeUs); - dest.writeInt(sentMediaQuality); - dest.writeByte((byte) (mp4Faststart ? 1 : 0)); - } - - @Override - public int describeContents() { - return 0; - } - - public static final Creator CREATOR = new Creator<>() { - @Override - public TransformProperties createFromParcel(Parcel in) { - return new TransformProperties(in); - } - - @Override - public TransformProperties[] newArray(int size) { - return new TransformProperties[size]; - } - }; - - public static @NonNull TransformProperties empty() { - return new TransformProperties(false, false, 0, 0, DEFAULT_MEDIA_QUALITY, false); - } - - public static @NonNull TransformProperties forSkipTransform() { - return new TransformProperties(true, false, 0, 0, DEFAULT_MEDIA_QUALITY, false); - } - - public static @NonNull TransformProperties forVideoTrim(long videoTrimStartTimeUs, long videoTrimEndTimeUs) { - return new TransformProperties(false, true, videoTrimStartTimeUs, videoTrimEndTimeUs, DEFAULT_MEDIA_QUALITY, false); - } - - public static @NonNull TransformProperties forSentMediaQuality(@NonNull Optional currentProperties, @NonNull SentMediaQuality sentMediaQuality) { - TransformProperties existing = currentProperties.orElse(empty()); - return new TransformProperties(existing.skipTransform, existing.videoTrim, existing.videoTrimStartTimeUs, existing.videoTrimEndTimeUs, sentMediaQuality.getCode(), existing.mp4Faststart); - } - - public boolean shouldSkipTransform() { - return skipTransform; - } - - public boolean isVideoEdited() { - return isVideoTrim(); - } - - public boolean isVideoTrim() { - return videoTrim; - } - - public boolean isMp4Faststart() { - return mp4Faststart; - } - - public long getVideoTrimStartTimeUs() { - return videoTrimStartTimeUs; - } - - public long getVideoTrimEndTimeUs() { - return videoTrimEndTimeUs; - } - - public int getSentMediaQuality() { - return sentMediaQuality; - } - - @NonNull TransformProperties withSkipTransform() { - return new TransformProperties(true, false, 0, 0, sentMediaQuality, false); - } - - @NonNull TransformProperties withMp4Faststart() { - return new TransformProperties(skipTransform, videoTrim, videoTrimStartTimeUs, videoTrimEndTimeUs, sentMediaQuality, true); - } - public @NonNull String serialize() { - return JsonUtil.toJson(this); - } - - static @NonNull TransformProperties parse(@Nullable String serialized) { - if (serialized == null) { - return empty(); - } - - try { - return JsonUtil.fromJson(serialized, TransformProperties.class); - } catch (IOException e) { - Log.w(TAG, "Failed to parse TransformProperties!", e); - return empty(); - } - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - final TransformProperties that = (TransformProperties) o; - return skipTransform == that.skipTransform && videoTrim == that.videoTrim && videoTrimStartTimeUs == that.videoTrimStartTimeUs && videoTrimEndTimeUs == that.videoTrimEndTimeUs && sentMediaQuality == that.sentMediaQuality; - } - - @Override - public int hashCode() { - return Objects.hash(skipTransform, videoTrim, videoTrimStartTimeUs, videoTrimEndTimeUs, sentMediaQuality); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentTable.kt new file mode 100644 index 0000000000..ca7fdd9a1c --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentTable.kt @@ -0,0 +1,1669 @@ +/* + * Copyright (C) 2011 Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.thoughtcrime.securesms.database + +import android.content.ContentValues +import android.content.Context +import android.database.Cursor +import android.media.MediaDataSource +import android.os.Parcelable +import android.text.TextUtils +import androidx.annotation.RequiresApi +import androidx.annotation.VisibleForTesting +import androidx.annotation.WorkerThread +import androidx.core.content.contentValuesOf +import com.bumptech.glide.Glide +import com.fasterxml.jackson.annotation.JsonProperty +import kotlinx.parcelize.IgnoredOnParcel +import kotlinx.parcelize.Parcelize +import org.json.JSONArray +import org.json.JSONException +import org.signal.core.util.Base64.encodeWithPadding +import org.signal.core.util.SqlUtil.buildArgs +import org.signal.core.util.SqlUtil.buildCollectionQuery +import org.signal.core.util.SqlUtil.buildSingleCollectionQuery +import org.signal.core.util.StreamUtil +import org.signal.core.util.ThreadUtil +import org.signal.core.util.delete +import org.signal.core.util.exists +import org.signal.core.util.forEach +import org.signal.core.util.groupBy +import org.signal.core.util.isNull +import org.signal.core.util.logging.Log +import org.signal.core.util.readToList +import org.signal.core.util.readToSingleObject +import org.signal.core.util.requireBlob +import org.signal.core.util.requireBoolean +import org.signal.core.util.requireInt +import org.signal.core.util.requireLong +import org.signal.core.util.requireNonNullBlob +import org.signal.core.util.requireNonNullString +import org.signal.core.util.requireString +import org.signal.core.util.select +import org.signal.core.util.update +import org.signal.core.util.withinTransaction +import org.thoughtcrime.securesms.attachments.Attachment +import org.thoughtcrime.securesms.attachments.AttachmentId +import org.thoughtcrime.securesms.attachments.DatabaseAttachment +import org.thoughtcrime.securesms.audio.AudioHash +import org.thoughtcrime.securesms.blurhash.BlurHash +import org.thoughtcrime.securesms.crypto.AttachmentSecret +import org.thoughtcrime.securesms.crypto.ClassicDecryptingPartInputStream +import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream +import org.thoughtcrime.securesms.crypto.ModernEncryptingPartOutputStream +import org.thoughtcrime.securesms.database.SignalDatabase.Companion.messages +import org.thoughtcrime.securesms.database.SignalDatabase.Companion.stickers +import org.thoughtcrime.securesms.database.SignalDatabase.Companion.threads +import org.thoughtcrime.securesms.database.model.databaseprotos.AudioWaveFormData +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies +import org.thoughtcrime.securesms.jobs.AttachmentDownloadJob +import org.thoughtcrime.securesms.jobs.GenerateAudioWaveFormJob +import org.thoughtcrime.securesms.mms.MediaStream +import org.thoughtcrime.securesms.mms.MmsException +import org.thoughtcrime.securesms.mms.PartAuthority +import org.thoughtcrime.securesms.mms.SentMediaQuality +import org.thoughtcrime.securesms.stickers.StickerLocator +import org.thoughtcrime.securesms.util.FileUtils +import org.thoughtcrime.securesms.util.JsonUtils.SaneJSONObject +import org.thoughtcrime.securesms.util.MediaUtil +import org.thoughtcrime.securesms.util.StorageUtil +import org.thoughtcrime.securesms.video.EncryptedMediaDataSource +import org.whispersystems.signalservice.internal.util.JsonUtil +import java.io.File +import java.io.FileNotFoundException +import java.io.IOException +import java.io.InputStream +import java.security.DigestInputStream +import java.security.MessageDigest +import java.security.NoSuchAlgorithmException +import java.util.Arrays +import java.util.LinkedList +import java.util.Objects +import java.util.Optional + +class AttachmentTable( + context: Context, + databaseHelper: SignalDatabase, + private val attachmentSecret: AttachmentSecret +) : DatabaseTable(context, databaseHelper) { + + companion object { + val TAG = Log.tag(AttachmentTable::class.java) + + const val TABLE_NAME = "part" + const val ROW_ID = "_id" + const val ATTACHMENT_JSON_ALIAS = "attachment_json" + const val MMS_ID = "mid" + const val CONTENT_TYPE = "ct" + const val NAME = "name" + const val CONTENT_DISPOSITION = "cd" + const val CONTENT_LOCATION = "cl" + const val DATA = "_data" + const val TRANSFER_STATE = "pending_push" + const val TRANSFER_FILE = "transfer_file" + const val SIZE = "data_size" + const val FILE_NAME = "file_name" + const val UNIQUE_ID = "unique_id" + const val DIGEST = "digest" + const val VOICE_NOTE = "voice_note" + const val BORDERLESS = "borderless" + const val VIDEO_GIF = "video_gif" + const val QUOTE = "quote" + const val STICKER_PACK_ID = "sticker_pack_id" + const val STICKER_PACK_KEY = "sticker_pack_key" + const val STICKER_ID = "sticker_id" + const val STICKER_EMOJI = "sticker_emoji" + const val FAST_PREFLIGHT_ID = "fast_preflight_id" + const val DATA_RANDOM = "data_random" + const val WIDTH = "width" + const val HEIGHT = "height" + const val CAPTION = "caption" + const val DATA_HASH = "data_hash" + const val VISUAL_HASH = "blur_hash" + const val TRANSFORM_PROPERTIES = "transform_properties" + const val DISPLAY_ORDER = "display_order" + const val UPLOAD_TIMESTAMP = "upload_timestamp" + const val CDN_NUMBER = "cdn_number" + const val MAC_DIGEST = "incremental_mac_digest" + const val INCREMENTAL_MAC_CHUNK_SIZE = "incremental_mac_chunk_size" + + private const val DIRECTORY = "parts" + + const val TRANSFER_PROGRESS_DONE = 0 + const val TRANSFER_PROGRESS_STARTED = 1 + const val TRANSFER_PROGRESS_PENDING = 2 + const val TRANSFER_PROGRESS_FAILED = 3 + const val TRANSFER_PROGRESS_PERMANENT_FAILURE = 4 + const val PREUPLOAD_MESSAGE_ID: Long = -8675309 + + private const val PART_ID_WHERE = "$ROW_ID = ? AND $UNIQUE_ID = ?" + private const val PART_ID_WHERE_NOT = "$ROW_ID != ? AND $UNIQUE_ID != ?" + + private val PROJECTION = arrayOf( + ROW_ID, + MMS_ID, + CONTENT_TYPE, + NAME, + CONTENT_DISPOSITION, + CDN_NUMBER, + CONTENT_LOCATION, + DATA, + TRANSFER_STATE, + SIZE, + FILE_NAME, + UNIQUE_ID, + DIGEST, + MAC_DIGEST, + INCREMENTAL_MAC_CHUNK_SIZE, + FAST_PREFLIGHT_ID, + VOICE_NOTE, + BORDERLESS, + VIDEO_GIF, + QUOTE, + DATA_RANDOM, + WIDTH, + HEIGHT, + CAPTION, + STICKER_PACK_ID, + STICKER_PACK_KEY, + STICKER_ID, + STICKER_EMOJI, + DATA_HASH, + VISUAL_HASH, + TRANSFORM_PROPERTIES, + TRANSFER_FILE, + DISPLAY_ORDER, + UPLOAD_TIMESTAMP + ) + + const val CREATE_TABLE = """ + CREATE TABLE $TABLE_NAME ( + $ROW_ID INTEGER PRIMARY KEY, + $MMS_ID INTEGER, + seq INTEGER DEFAULT 0, + $CONTENT_TYPE TEXT, + $NAME TEXT, + chset INTEGER, + $CONTENT_DISPOSITION TEXT, + fn TEXT, + cid TEXT, + $CONTENT_LOCATION TEXT, + ctt_s INTEGER, + ctt_t TEXT, + encrypted INTEGER, + $TRANSFER_STATE INTEGER, + $DATA TEXT, + $SIZE INTEGER, + $FILE_NAME TEXT, + $UNIQUE_ID INTEGER NOT NULL, + $DIGEST BLOB, + $FAST_PREFLIGHT_ID TEXT, + $VOICE_NOTE INTEGER DEFAULT 0, + $BORDERLESS INTEGER DEFAULT 0, + $VIDEO_GIF INTEGER DEFAULT 0, + $DATA_RANDOM BLOB, + $QUOTE INTEGER DEFAULT 0, + $WIDTH INTEGER DEFAULT 0, + $HEIGHT INTEGER DEFAULT 0, + $CAPTION TEXT DEFAULT NULL, + $STICKER_PACK_ID TEXT DEFAULT NULL, + $STICKER_PACK_KEY DEFAULT NULL, + $STICKER_ID INTEGER DEFAULT -1, + $STICKER_EMOJI STRING DEFAULT NULL, + $DATA_HASH TEXT DEFAULT NULL, + $VISUAL_HASH TEXT DEFAULT NULL, + $TRANSFORM_PROPERTIES TEXT DEFAULT NULL, + $TRANSFER_FILE TEXT DEFAULT NULL, + $DISPLAY_ORDER INTEGER DEFAULT 0, + $UPLOAD_TIMESTAMP INTEGER DEFAULT 0, + $CDN_NUMBER INTEGER DEFAULT 0, + $MAC_DIGEST BLOB, + $INCREMENTAL_MAC_CHUNK_SIZE INTEGER DEFAULT 0 + ) + """ + + @JvmField + val CREATE_INDEXS = arrayOf( + "CREATE INDEX IF NOT EXISTS part_mms_id_index ON $TABLE_NAME ($MMS_ID);", + "CREATE INDEX IF NOT EXISTS pending_push_index ON $TABLE_NAME ($TRANSFER_STATE);", + "CREATE INDEX IF NOT EXISTS part_sticker_pack_id_index ON $TABLE_NAME ($STICKER_PACK_ID);", + "CREATE INDEX IF NOT EXISTS part_data_hash_index ON $TABLE_NAME ($DATA_HASH);", + "CREATE INDEX IF NOT EXISTS part_data_index ON $TABLE_NAME ($DATA);" + ) + + @JvmStatic + @JvmOverloads + @Throws(IOException::class) + fun newFile(context: Context): File { + val partsDirectory = context.getDir(DIRECTORY, Context.MODE_PRIVATE) + return PartFileProtector.protect { File.createTempFile("part", ".mms", partsDirectory) } + } + } + + @Throws(IOException::class) + fun getAttachmentStream(attachmentId: AttachmentId, offset: Long): InputStream { + return try { + getDataStream(attachmentId, DATA, offset) + } catch (e: FileNotFoundException) { + throw IOException("No stream for: $attachmentId", e) + } ?: throw IOException("No stream for: $attachmentId") + } + + fun getAttachment(attachmentId: AttachmentId): DatabaseAttachment? { + return readableDatabase + .select(*PROJECTION) + .from(TABLE_NAME) + .where(PART_ID_WHERE, attachmentId.toStrings()) + .run() + .readToList { it.readAttachments() } + .flatten() + .firstOrNull() + } + + fun getAttachmentsForMessage(mmsId: Long): List { + return readableDatabase + .select(*PROJECTION) + .from(TABLE_NAME) + .where("$MMS_ID = ?", mmsId) + .orderBy("$UNIQUE_ID ASC, $ROW_ID ASC") + .run() + .readToList { it.readAttachments() } + .flatten() + } + + fun getAttachmentsForMessages(mmsIds: Collection): Map> { + if (mmsIds.isEmpty()) { + return emptyMap() + } + + val query = buildSingleCollectionQuery(MMS_ID, mmsIds) + + return readableDatabase + .select(*PROJECTION) + .from(TABLE_NAME) + .where(query.where, query.whereArgs) + .orderBy("$UNIQUE_ID ASC, $ROW_ID ASC") + .run() + .groupBy { cursor -> + val attachment = cursor.readAttachment() + attachment.mmsId to attachment + } + } + + fun hasAttachment(id: AttachmentId): Boolean { + return readableDatabase + .exists(TABLE_NAME) + .where(PART_ID_WHERE, id.toStrings()) + .run() + } + + fun getPendingAttachments(): List { + return readableDatabase + .select(*PROJECTION) + .from(TABLE_NAME) + .where("$TRANSFER_STATE = ?", TRANSFER_PROGRESS_STARTED.toString()) + .run() + .readToList { it.readAttachments() } + .flatten() + } + + fun deleteAttachmentsForMessage(mmsId: Long): Boolean { + Log.d(TAG, "[deleteAttachmentsForMessage] mmsId: $mmsId") + + return writableDatabase.withinTransaction { db -> + db.select(DATA, CONTENT_TYPE, ROW_ID, UNIQUE_ID) + .from(TABLE_NAME) + .where("$MMS_ID = ?", mmsId) + .run() + .forEach { cursor -> + val attachmentId = AttachmentId( + cursor.requireLong(ROW_ID), + cursor.requireLong(UNIQUE_ID) + ) + + ApplicationDependencies.getJobManager().cancelAllInQueue(AttachmentDownloadJob.constructQueueString(attachmentId)) + + deleteAttachmentOnDisk( + data = cursor.requireString(DATA), + contentType = cursor.requireString(CONTENT_TYPE), + attachmentId = attachmentId + ) + } + + val deleteCount = db.delete(TABLE_NAME) + .where("$MMS_ID = ?", mmsId) + .run() + + notifyAttachmentListeners() + + deleteCount > 0 + } + } + + /** + * Deletes all attachments with an ID of [PREUPLOAD_MESSAGE_ID]. These represent + * attachments that were pre-uploaded and haven't been assigned to a message. This should only be + * done when you *know* that all attachments *should* be assigned a real mmsId. For instance, when + * the app starts. Otherwise you could delete attachments that are legitimately being + * pre-uploaded. + */ + fun deleteAbandonedPreuploadedAttachments(): Int { + var count = 0 + + writableDatabase + .select(ROW_ID, UNIQUE_ID) + .from(TABLE_NAME) + .where("$MMS_ID = ?", PREUPLOAD_MESSAGE_ID) + .run() + .forEach { cursor -> + val id = AttachmentId( + cursor.requireLong(ROW_ID), + cursor.requireLong(UNIQUE_ID) + ) + + deleteAttachment(id) + + count++ + } + + return count + } + + fun deleteAttachmentFilesForViewOnceMessage(mmsId: Long) { + Log.d(TAG, "[deleteAttachmentFilesForViewOnceMessage] mmsId: $mmsId") + + writableDatabase.withinTransaction { db -> + db.select(DATA, CONTENT_TYPE, ROW_ID, UNIQUE_ID) + .from(TABLE_NAME) + .where("$MMS_ID = ?", mmsId) + .run() + .forEach { cursor -> + deleteAttachmentOnDisk( + data = cursor.requireString(DATA), + contentType = cursor.requireString(CONTENT_TYPE), + attachmentId = AttachmentId(cursor.requireLong(ROW_ID), cursor.requireLong(UNIQUE_ID)) + ) + } + + db.update(TABLE_NAME) + .values( + DATA to null, + DATA_RANDOM to null, + DATA_HASH to null, + FILE_NAME to null, + CAPTION to null, + SIZE to 0, + WIDTH to 0, + HEIGHT to 0, + TRANSFER_STATE to TRANSFER_PROGRESS_DONE, + VISUAL_HASH to null, + CONTENT_TYPE to MediaUtil.VIEW_ONCE + ) + .run() + + notifyAttachmentListeners() + + val threadId = messages.getThreadIdForMessage(mmsId) + if (threadId > 0) { + notifyConversationListeners(threadId) + } + } + } + + fun deleteAttachment(id: AttachmentId) { + Log.d(TAG, "[deleteAttachment] attachmentId: $id") + + writableDatabase.withinTransaction { db -> + db.select(DATA, CONTENT_TYPE) + .from(TABLE_NAME) + .where(PART_ID_WHERE, id.toStrings()) + .run() + .use { cursor -> + if (!cursor.moveToFirst()) { + Log.w(TAG, "Tried to delete an attachment, but it didn't exist.") + return@withinTransaction + } + + val data = cursor.requireString(DATA) + val contentType = cursor.requireString(CONTENT_TYPE) + + deleteAttachmentOnDisk( + data = data, + contentType = contentType, + attachmentId = id + ) + + db.delete(TABLE_NAME) + .where(PART_ID_WHERE, id.toStrings()) + .run() + + deleteAttachmentOnDisk(data, contentType, id) + notifyAttachmentListeners() + } + } + } + + fun trimAllAbandonedAttachments() { + val deleteCount = writableDatabase + .delete(TABLE_NAME) + .where("$MMS_ID != $PREUPLOAD_MESSAGE_ID AND $MMS_ID NOT IN (SELECT ${MessageTable.ID} FROM ${MessageTable.TABLE_NAME})") + .run() + + if (deleteCount > 0) { + Log.i(TAG, "Trimmed $deleteCount abandoned attachments.") + } + } + + fun deleteAbandonedAttachmentFiles(): Int { + val diskFiles = context.getDir(DIRECTORY, Context.MODE_PRIVATE).listFiles() ?: return 0 + + val filesOnDisk: Set = diskFiles + .filter { file: File -> !PartFileProtector.isProtected(file) } + .map { file: File -> file.absolutePath } + .toSet() + + val filesInDb: Set = readableDatabase + .select(DATA) + .from(TABLE_NAME) + .run() + .readToList { it.requireString(DATA) } + .filterNotNull() + .toSet() + stickers.allStickerFiles + + val onDiskButNotInDatabase: Set = filesOnDisk - filesInDb + + for (filePath in onDiskButNotInDatabase) { + val success = File(filePath).delete() + if (!success) { + Log.w(TAG, "[deleteAbandonedAttachmentFiles] Failed to delete attachment file. $filePath") + } + } + + return onDiskButNotInDatabase.size + } + + fun deleteAllAttachments() { + Log.d(TAG, "[deleteAllAttachments]") + + writableDatabase + .delete(TABLE_NAME) + .run() + + FileUtils.deleteDirectoryContents(context.getDir(DIRECTORY, Context.MODE_PRIVATE)) + + notifyAttachmentListeners() + } + + fun setTransferState(messageId: Long, attachmentId: AttachmentId, transferState: Int) { + writableDatabase + .update(TABLE_NAME) + .values(TRANSFER_STATE to transferState) + .where(PART_ID_WHERE, attachmentId.toStrings()) + .run() + + val threadId = messages.getThreadIdForMessage(messageId) + notifyConversationListeners(threadId) + } + + @Throws(MmsException::class) + fun setTransferProgressFailed(attachmentId: AttachmentId, mmsId: Long) { + writableDatabase + .update(TABLE_NAME) + .values(TRANSFER_STATE to TRANSFER_PROGRESS_FAILED) + .where("$PART_ID_WHERE AND $TRANSFER_STATE < $TRANSFER_PROGRESS_PERMANENT_FAILURE", attachmentId.toStrings()) + .run() + + notifyConversationListeners(messages.getThreadIdForMessage(mmsId)) + } + + @Throws(MmsException::class) + fun setTransferProgressPermanentFailure(attachmentId: AttachmentId, mmsId: Long) { + writableDatabase + .update(TABLE_NAME) + .values(TRANSFER_STATE to TRANSFER_PROGRESS_PERMANENT_FAILURE) + .where(PART_ID_WHERE, attachmentId.toStrings()) + .run() + + notifyConversationListeners(messages.getThreadIdForMessage(mmsId)) + } + + @Throws(MmsException::class) + fun insertAttachmentsForPlaceholder(mmsId: Long, attachmentId: AttachmentId, inputStream: InputStream) { + val placeholder = getAttachment(attachmentId) + val oldInfo = getAttachmentDataFileInfo(attachmentId, DATA) + var dataInfo = storeAttachmentStream(inputStream) + val transferFile = getTransferFile(databaseHelper.signalReadableDatabase, attachmentId) + + val updated = writableDatabase.withinTransaction { db -> + dataInfo = deduplicateAttachment(dataInfo, attachmentId, placeholder?.transformProperties ?: TransformProperties.empty()) + + if (oldInfo != null) { + updateAttachmentDataHash(db, oldInfo.hash, dataInfo) + } + + val values = ContentValues() + values.put(DATA, dataInfo.file.absolutePath) + values.put(SIZE, dataInfo.length) + values.put(DATA_RANDOM, dataInfo.random) + values.put(DATA_HASH, dataInfo.hash) + + val visualHashString = placeholder.getVisualHashStringOrNull() + if (visualHashString != null) { + values.put(VISUAL_HASH, visualHashString) + } + + values.put(TRANSFER_STATE, TRANSFER_PROGRESS_DONE) + values.put(TRANSFER_FILE, null as String?) + values.put(TRANSFORM_PROPERTIES, TransformProperties.forSkipTransform().serialize()) + + val updateCount = db.update(TABLE_NAME) + .values(values) + .where(PART_ID_WHERE, attachmentId.toStrings()) + .run() + + updateCount > 0 + } + + if (updated) { + val threadId = messages.getThreadIdForMessage(mmsId) + + if (!messages.isStory(mmsId)) { + threads.updateSnippetUriSilently(threadId, PartAuthority.getAttachmentDataUri(attachmentId)) + } + + notifyConversationListeners(threadId) + notifyConversationListListeners() + notifyAttachmentListeners() + } else { + if (!dataInfo.file.delete()) { + Log.w(TAG, "Failed to delete unused attachment") + } + } + + if (transferFile != null) { + if (!transferFile.delete()) { + Log.w(TAG, "Unable to delete transfer file.") + } + } + + if (placeholder != null && MediaUtil.isAudio(placeholder)) { + GenerateAudioWaveFormJob.enqueue(placeholder.attachmentId) + } + } + + @Throws(MmsException::class) + fun copyAttachmentData(sourceId: AttachmentId, destinationId: AttachmentId) { + val sourceAttachment = getAttachment(sourceId) ?: throw MmsException("Cannot find attachment for source!") + val sourceDataInfo = getAttachmentDataFileInfo(sourceId, DATA) ?: throw MmsException("No attachment data found for source!") + + writableDatabase + .update(TABLE_NAME) + .values( + DATA to sourceDataInfo.file.absolutePath, + DATA_HASH to sourceDataInfo.hash, + SIZE to sourceDataInfo.length, + DATA_RANDOM to sourceDataInfo.random, + TRANSFER_STATE to sourceAttachment.transferState, + CDN_NUMBER to sourceAttachment.cdnNumber, + CONTENT_LOCATION to sourceAttachment.location, + DIGEST to sourceAttachment.digest, + MAC_DIGEST to sourceAttachment.incrementalDigest, + INCREMENTAL_MAC_CHUNK_SIZE to sourceAttachment.incrementalMacChunkSize, + CONTENT_DISPOSITION to sourceAttachment.key, + NAME to sourceAttachment.relay, + SIZE to sourceAttachment.size, + FAST_PREFLIGHT_ID to sourceAttachment.fastPreflightId, + WIDTH to sourceAttachment.width, + HEIGHT to sourceAttachment.height, + CONTENT_TYPE to sourceAttachment.contentType, + VISUAL_HASH to sourceAttachment.getVisualHashStringOrNull(), + TRANSFORM_PROPERTIES to sourceAttachment.transformProperties?.serialize() + ) + .where(PART_ID_WHERE, destinationId.toStrings()) + .run() + } + + fun updateAttachmentCaption(id: AttachmentId, caption: String?) { + writableDatabase + .update(TABLE_NAME) + .values(CAPTION to caption) + .where(PART_ID_WHERE, id.toStrings()) + .run() + } + + fun updateDisplayOrder(orderMap: Map) { + writableDatabase.withinTransaction { db -> + for ((key, value) in orderMap) { + db.update(TABLE_NAME) + .values(DISPLAY_ORDER to value) + .where(PART_ID_WHERE, key.toStrings()) + .run() + } + } + } + + fun updateAttachmentAfterUpload(id: AttachmentId, attachment: Attachment, uploadTimestamp: Long) { + val dataInfo = getAttachmentDataFileInfo(id, DATA) + val values = contentValuesOf( + TRANSFER_STATE to TRANSFER_PROGRESS_DONE, + CDN_NUMBER to attachment.cdnNumber, + CONTENT_LOCATION to attachment.location, + DIGEST to attachment.digest, + MAC_DIGEST to attachment.incrementalDigest, + INCREMENTAL_MAC_CHUNK_SIZE to attachment.incrementalMacChunkSize, + CONTENT_DISPOSITION to attachment.key, + NAME to attachment.relay, + SIZE to attachment.size, + FAST_PREFLIGHT_ID to attachment.fastPreflightId, + VISUAL_HASH to attachment.getVisualHashStringOrNull(), + UPLOAD_TIMESTAMP to uploadTimestamp + ) + + if (dataInfo != null && dataInfo.hash != null) { + updateAttachmentAndMatchingHashes(writableDatabase, id, dataInfo.hash, values) + } else { + writableDatabase + .update(TABLE_NAME) + .values(values) + .where(PART_ID_WHERE, id.toStrings()) + .run() + } + } + + @Throws(MmsException::class) + fun insertAttachmentForPreUpload(attachment: Attachment): DatabaseAttachment { + val result = insertAttachmentsForMessage(PREUPLOAD_MESSAGE_ID, listOf(attachment), emptyList()) + + if (result.values.isEmpty()) { + throw MmsException("Bad attachment result!") + } + + return getAttachment(result.values.iterator().next()) ?: throw MmsException("Failed to retrieve attachment we just inserted!") + } + + fun updateMessageId(attachmentIds: Collection, mmsId: Long, isStory: Boolean) { + writableDatabase.withinTransaction { db -> + val values = ContentValues(2).apply { + put(MMS_ID, mmsId) + if (!isStory) { + putNull(CAPTION) + } + } + + var updatedCount = 0 + var attachmentIdSize = 0 + for (attachmentId in attachmentIds) { + attachmentIdSize++ + updatedCount += db + .update(TABLE_NAME) + .values(values) + .where(PART_ID_WHERE, attachmentId.toStrings()) + .run() + } + + Log.d(TAG, "[updateMessageId] Updated $updatedCount out of $attachmentIdSize ids.") + } + } + + @Throws(MmsException::class) + fun insertAttachmentsForMessage(mmsId: Long, attachments: List, quoteAttachment: List): Map { + if (attachments.isEmpty() && quoteAttachment.isEmpty()) { + return emptyMap() + } + + Log.d(TAG, "insertParts(${attachments.size})") + + val insertedAttachments: MutableMap = mutableMapOf() + for (attachment in attachments) { + val attachmentId = insertAttachment(mmsId, attachment, attachment.quote) + insertedAttachments[attachment] = attachmentId + Log.i(TAG, "Inserted attachment at ID: $attachmentId") + } + + try { + for (attachment in quoteAttachment) { + val attachmentId = insertAttachment(mmsId, attachment, true) + insertedAttachments[attachment] = attachmentId + Log.i(TAG, "Inserted quoted attachment at ID: $attachmentId") + } + } catch (e: MmsException) { + Log.w(TAG, "Failed to insert quote attachment! messageId: $mmsId") + } + + return insertedAttachments + } + + /** + * @param onlyModifyThisAttachment If false and more than one attachment shares this file and quality, they will all + * be updated. If true, then guarantees not to affect other attachments. + */ + @Throws(MmsException::class, IOException::class) + fun updateAttachmentData( + databaseAttachment: DatabaseAttachment, + mediaStream: MediaStream, + onlyModifyThisAttachment: Boolean + ) { + val attachmentId = databaseAttachment.attachmentId + val oldDataInfo = getAttachmentDataFileInfo(attachmentId, DATA) ?: throw MmsException("No attachment data found!") + var destination = oldDataInfo.file + val isSingleUseOfData = onlyModifyThisAttachment || oldDataInfo.hash == null + + if (isSingleUseOfData && fileReferencedByMoreThanOneAttachment(destination)) { + Log.i(TAG, "Creating a new file as this one is used by more than one attachment") + destination = newFile(context) + } + + var dataInfo: DataInfo = storeAttachmentStream(destination, mediaStream.stream) + + writableDatabase.withinTransaction { db -> + dataInfo = deduplicateAttachment(dataInfo, attachmentId, databaseAttachment.transformProperties) + + val contentValues = contentValuesOf( + SIZE to dataInfo.length, + CONTENT_TYPE to mediaStream.mimeType, + WIDTH to mediaStream.width, + HEIGHT to mediaStream.height, + DATA to dataInfo.file.absolutePath, + DATA_RANDOM to dataInfo.random, + DATA_HASH to dataInfo.hash + ) + + val updateCount = updateAttachmentAndMatchingHashes( + db = db, + attachmentId = attachmentId, + dataHash = if (isSingleUseOfData) dataInfo.hash else oldDataInfo.hash, + contentValues = contentValues + ) + + Log.i(TAG, "[updateAttachmentData] Updated $updateCount rows.") + } + } + + fun duplicateAttachmentsForMessage(destinationMessageId: Long, sourceMessageId: Long, excludedIds: Collection) { + writableDatabase.withinTransaction { db -> + db.execSQL("CREATE TEMPORARY TABLE tmp_part AS SELECT * FROM $TABLE_NAME WHERE $MMS_ID = ?", buildArgs(sourceMessageId)) + + val queries = buildCollectionQuery(ROW_ID, excludedIds) + for (query in queries) { + db.delete("tmp_part", query.where, query.whereArgs) + } + + db.execSQL("UPDATE tmp_part SET $ROW_ID = NULL, $MMS_ID = ?", buildArgs(destinationMessageId)) + db.execSQL("INSERT INTO $TABLE_NAME SELECT * FROM tmp_part") + db.execSQL("DROP TABLE tmp_part") + } + } + + @Throws(IOException::class) + fun getOrCreateTransferFile(attachmentId: AttachmentId): File { + val existing = getTransferFile(writableDatabase, attachmentId) + if (existing != null) { + return existing + } + + val transferFile = newTransferFile() + + writableDatabase + .update(TABLE_NAME) + .values(TRANSFER_FILE to transferFile.absolutePath) + .where(PART_ID_WHERE, attachmentId.toStrings()) + .run() + + return transferFile + } + + @VisibleForTesting + fun getAttachmentDataFileInfo(attachmentId: AttachmentId, dataType: String): DataInfo? { + return readableDatabase + .select(dataType, SIZE, DATA_RANDOM, DATA_HASH, TRANSFORM_PROPERTIES) + .from(TABLE_NAME) + .where(PART_ID_WHERE, attachmentId.toStrings()) + .run() + .readToSingleObject { cursor -> + if (cursor.isNull(dataType)) { + null + } else { + DataInfo( + file = File(cursor.getString(cursor.getColumnIndexOrThrow(dataType))), + length = cursor.requireLong(SIZE), + random = cursor.requireNonNullBlob(DATA_RANDOM), + hash = cursor.requireNonNullString(DATA_HASH), + transformProperties = TransformProperties.parse(cursor.requireString(TRANSFORM_PROPERTIES)) + ) + } + } + } + + fun markAttachmentAsTransformed(attachmentId: AttachmentId, withFastStart: Boolean) { + writableDatabase.withinTransaction { db -> + try { + var transformProperties = getTransformProperties(attachmentId) + if (transformProperties == null) { + Log.w(TAG, "Failed to get transformation properties, attachment no longer exists.") + return@withinTransaction + } + + transformProperties = transformProperties.withSkipTransform() + if (withFastStart) { + transformProperties = transformProperties.withMp4FastStart() + } + + updateAttachmentTransformProperties(attachmentId, transformProperties) + } catch (e: Exception) { + Log.w(TAG, "Could not mark attachment as transformed.", e) + } + } + } + + @WorkerThread + fun writeAudioHash(attachmentId: AttachmentId, audioWaveForm: AudioWaveFormData?) { + Log.i(TAG, "updating part audio wave form for $attachmentId") + writableDatabase + .update(TABLE_NAME) + .values(VISUAL_HASH to audioWaveForm?.let { AudioHash(it).hash }) + .where(PART_ID_WHERE, attachmentId.toStrings()) + .run() + } + + @RequiresApi(23) + fun mediaDataSourceFor(attachmentId: AttachmentId, allowReadingFromTempFile: Boolean): MediaDataSource? { + val dataInfo = getAttachmentDataFileInfo(attachmentId, DATA) + if (dataInfo != null) { + return EncryptedMediaDataSource.createFor(attachmentSecret, dataInfo.file, dataInfo.random, dataInfo.length) + } + + if (allowReadingFromTempFile) { + Log.d(TAG, "Completed data file not found for video attachment, checking for in-progress files.") + val transferFile = getTransferFile(readableDatabase, attachmentId) + if (transferFile != null) { + return EncryptedMediaDataSource.createForDiskBlob(attachmentSecret, transferFile) + } + } + + Log.w(TAG, "No data file found for video attachment!") + return null + } + + /** + * @return null if we fail to find the given attachmentId + */ + fun getTransformProperties(attachmentId: AttachmentId): TransformProperties? { + return readableDatabase + .select(TRANSFORM_PROPERTIES) + .from(TABLE_NAME) + .where(PART_ID_WHERE, attachmentId.toStrings()) + .run() + .readToSingleObject { + TransformProperties.parse(it.requireString(TRANSFORM_PROPERTIES)) + } + } + + fun markAttachmentUploaded(messageId: Long, attachment: Attachment) { + writableDatabase + .update(TABLE_NAME) + .values(TRANSFER_STATE to TRANSFER_PROGRESS_DONE) + .where(PART_ID_WHERE, (attachment as DatabaseAttachment).attachmentId.toStrings()) + .run() + + val threadId = messages.getThreadIdForMessage(messageId) + notifyConversationListeners(threadId) + } + + fun getAttachments(cursor: Cursor): List { + return try { + if (cursor.getColumnIndex(ATTACHMENT_JSON_ALIAS) != -1) { + if (cursor.isNull(ATTACHMENT_JSON_ALIAS)) { + return LinkedList() + } + + val result: MutableList = mutableListOf() + val array = JSONArray(cursor.requireString(ATTACHMENT_JSON_ALIAS)) + + for (i in 0 until array.length()) { + val jsonObject = SaneJSONObject(array.getJSONObject(i)) + + if (!jsonObject.isNull(ROW_ID)) { + val contentType = jsonObject.getString(CONTENT_TYPE) + + result += DatabaseAttachment( + attachmentId = AttachmentId(jsonObject.getLong(ROW_ID), jsonObject.getLong(UNIQUE_ID)), + mmsId = jsonObject.getLong(MMS_ID), + hasData = !TextUtils.isEmpty(jsonObject.getString(DATA)), + hasThumbnail = MediaUtil.isImageType(contentType) || MediaUtil.isVideoType(contentType), + contentType = contentType, + transferProgress = jsonObject.getInt(TRANSFER_STATE), + size = jsonObject.getLong(SIZE), + fileName = jsonObject.getString(FILE_NAME), + cdnNumber = jsonObject.getInt(CDN_NUMBER), + location = jsonObject.getString(CONTENT_LOCATION), + key = jsonObject.getString(CONTENT_DISPOSITION), + relay = jsonObject.getString(NAME), + digest = null, + incrementalDigest = null, + incrementalMacChunkSize = 0, + fastPreflightId = jsonObject.getString(FAST_PREFLIGHT_ID), + voiceNote = jsonObject.getInt(VOICE_NOTE) == 1, + borderless = jsonObject.getInt(BORDERLESS) == 1, + videoGif = jsonObject.getInt(VIDEO_GIF) == 1, + width = jsonObject.getInt(WIDTH), + height = jsonObject.getInt(HEIGHT), + quote = jsonObject.getInt(QUOTE) == 1, + caption = jsonObject.getString(CAPTION), + stickerLocator = if (jsonObject.getInt(STICKER_ID) >= 0) { + StickerLocator( + jsonObject.getString(STICKER_PACK_ID)!!, + jsonObject.getString(STICKER_PACK_KEY)!!, + jsonObject.getInt(STICKER_ID), + jsonObject.getString(STICKER_EMOJI) + ) + } else { + null + }, + blurHash = if (MediaUtil.isAudioType(contentType)) null else BlurHash.parseOrNull(jsonObject.getString(VISUAL_HASH)), + audioHash = if (MediaUtil.isAudioType(contentType)) AudioHash.parseOrNull(jsonObject.getString(VISUAL_HASH)) else null, + transformProperties = TransformProperties.parse(jsonObject.getString(TRANSFORM_PROPERTIES)), + displayOrder = jsonObject.getInt(DISPLAY_ORDER), + uploadTimestamp = jsonObject.getLong(UPLOAD_TIMESTAMP) + ) + } + } + + result + } else { + listOf(getAttachment(cursor)) + } + } catch (e: JSONException) { + throw AssertionError(e) + } + } + + fun hasStickerAttachments(): Boolean { + return readableDatabase + .exists(TABLE_NAME) + .where("$STICKER_PACK_ID NOT NULL") + .run() + } + + fun containsStickerPackId(stickerPackId: String): Boolean { + return readableDatabase.exists(TABLE_NAME) + .where("$STICKER_PACK_ID = ?", stickerPackId) + .run() + } + + fun getUnavailableStickerPacks(): Cursor { + val query = """ + SELECT DISTINCT $STICKER_PACK_ID, $STICKER_PACK_KEY + FROM $TABLE_NAME + WHERE + $STICKER_PACK_ID NOT NULL AND + $STICKER_PACK_KEY NOT NULL AND + $STICKER_PACK_ID NOT IN (SELECT DISTINCT ${StickerTable.PACK_ID} FROM ${StickerTable.TABLE_NAME}) + """ + + return readableDatabase.rawQuery(query, null) + } + + private fun deleteAttachmentOnDisk( + data: String?, + contentType: String?, + attachmentId: AttachmentId + ) { + check(writableDatabase.inTransaction()) { "Must be in a transaction!" } + + val dataUsage = getAttachmentFileUsages(data, attachmentId) + if (dataUsage.hasStrongReference) { + Log.i(TAG, "[deleteAttachmentOnDisk] Attachment in use. Skipping deletion. $data $attachmentId") + return + } + + Log.i(TAG, "[deleteAttachmentOnDisk] No other strong uses of this attachment. Safe to delete. $data $attachmentId") + if (!data.isNullOrBlank()) { + if (File(data).delete()) { + Log.i(TAG, "[deleteAttachmentOnDisk] Deleted attachment file. $data $attachmentId") + + if (dataUsage.removableWeakReferences.isNotEmpty()) { + Log.i(TAG, "[deleteAttachmentOnDisk] Deleting ${dataUsage.removableWeakReferences.size} weak references for $data") + + var deletedCount = 0 + for (weakReference in dataUsage.removableWeakReferences) { + Log.i(TAG, "[deleteAttachmentOnDisk] Clearing weak reference for $data $weakReference") + + deletedCount += writableDatabase + .update(TABLE_NAME) + .values( + DATA to null, + DATA_RANDOM to null, + DATA_HASH to null + ) + .where(PART_ID_WHERE, weakReference.toStrings()) + .run() + } + + val logMessage = "[deleteAttachmentOnDisk] Cleared $deletedCount/${dataUsage.removableWeakReferences.size} weak references for $data" + if (deletedCount != dataUsage.removableWeakReferences.size) { + Log.w(TAG, logMessage) + } else { + Log.i(TAG, logMessage) + } + } + } else { + Log.w(TAG, "[deleteAttachmentOnDisk] Failed to delete attachment. $data $attachmentId") + } + } + + if (MediaUtil.isImageType(contentType) || MediaUtil.isVideoType(contentType)) { + Glide.get(context).clearDiskCache() + ThreadUtil.runOnMain { Glide.get(context).clearMemory() } + } + } + + private fun getAttachmentFileUsages(data: String?, attachmentId: AttachmentId): DataUsageResult { + check(writableDatabase.inTransaction()) { "Must be in a transaction!" } + + if (data == null) { + return DataUsageResult.NOT_IN_USE + } + + val quoteRows: MutableList = mutableListOf() + + readableDatabase + .select(ROW_ID, UNIQUE_ID, QUOTE) + .from(TABLE_NAME) + .where("$DATA = ? AND $UNIQUE_ID != ? AND $ROW_ID != ?", data, attachmentId.uniqueId, attachmentId.rowId) + .run() + .forEach { cursor -> + if (cursor.requireBoolean(QUOTE)) { + quoteRows += AttachmentId(cursor.requireLong(ROW_ID), cursor.requireLong(UNIQUE_ID)) + } else { + return DataUsageResult.IN_USE + } + } + + return DataUsageResult(quoteRows) + } + + /** + * Check if data file is in use by another attachment row with a different hash. Rows with the same data and hash + * will be fixed in a later call to [updateAttachmentAndMatchingHashes]. + */ + private fun isAttachmentFileUsedByOtherAttachments(attachmentId: AttachmentId?, dataInfo: DataInfo): Boolean { + return if (attachmentId == null) { + false + } else { + readableDatabase + .exists(TABLE_NAME) + .where("$DATA = ? AND $DATA_HASH != ?", dataInfo.file.absolutePath, dataInfo.hash) + .run() + } + } + + private fun updateAttachmentDataHash( + db: SQLiteDatabase, + oldHash: String, + newData: DataInfo + ) { + if (oldHash == null) { + return + } + + db.update(TABLE_NAME) + .values( + DATA to newData.file.absolutePath, + DATA_RANDOM to newData.random, + DATA_HASH to newData.hash + ) + .where("$DATA_HASH = ?", oldHash) + .run() + } + + private fun updateAttachmentTransformProperties(attachmentId: AttachmentId, transformProperties: TransformProperties) { + val dataInfo = getAttachmentDataFileInfo(attachmentId, DATA) + if (dataInfo == null) { + Log.w(TAG, "[updateAttachmentTransformProperties] No data info found!") + return + } + + val contentValues = contentValuesOf(TRANSFORM_PROPERTIES to transformProperties.serialize()) + val updateCount = updateAttachmentAndMatchingHashes(databaseHelper.signalWritableDatabase, attachmentId, dataInfo.hash, contentValues) + Log.i(TAG, "[updateAttachmentTransformProperties] Updated $updateCount rows.") + } + + private fun updateAttachmentAndMatchingHashes( + db: SQLiteDatabase, + attachmentId: AttachmentId, + dataHash: String?, + contentValues: ContentValues + ): Int { + return db + .update(TABLE_NAME) + .values(contentValues) + .where("($ROW_ID = ? AND $UNIQUE_ID = ?) OR ($DATA_HASH NOT NULL AND $DATA_HASH = ?)", attachmentId.rowId.toString(), attachmentId.uniqueId.toString(), dataHash.toString()) + .run() + } + + /** + * Returns true if the file referenced by two or more attachments. + * Returns false if the file is referenced by zero or one attachments. + */ + private fun fileReferencedByMoreThanOneAttachment(file: File): Boolean { + return readableDatabase + .select("1") + .from(TABLE_NAME) + .where("$DATA = ?", file.absolutePath) + .limit(2) + .run() + .use { cursor -> + cursor.moveToNext() && cursor.moveToNext() + } + } + + @Throws(FileNotFoundException::class) + private fun getDataStream(attachmentId: AttachmentId, dataType: String, offset: Long): InputStream? { + val dataInfo = getAttachmentDataFileInfo(attachmentId, dataType) ?: return null + + return try { + if (dataInfo.random != null && dataInfo.random.size == 32) { + ModernDecryptingPartInputStream.createFor(attachmentSecret, dataInfo.random, dataInfo.file, offset) + } else { + val stream = ClassicDecryptingPartInputStream.createFor(attachmentSecret, dataInfo.file) + val skipped = stream.skip(offset) + if (skipped != offset) { + Log.w(TAG, "Skip failed: $skipped vs $offset") + return null + } + stream + } + } catch (e: FileNotFoundException) { + Log.w(TAG, e) + throw e + } catch (e: IOException) { + Log.w(TAG, e) + null + } + } + + @Throws(MmsException::class) + private fun storeAttachmentStream(inputStream: InputStream): DataInfo { + return try { + storeAttachmentStream(newFile(context), inputStream) + } catch (e: IOException) { + throw MmsException(e) + } + } + + @Throws(IOException::class) + private fun newTransferFile(): File { + val partsDirectory = context.getDir(DIRECTORY, Context.MODE_PRIVATE) + return PartFileProtector.protect { + File.createTempFile("transfer", ".mms", partsDirectory) + } + } + + /** + * Reads the entire stream and saves to disk. If you need to deduplicate attachments, call [deduplicateAttachment] + * afterwards and use the [DataInfo] returned by it instead. + */ + @Throws(MmsException::class, IllegalStateException::class) + private fun storeAttachmentStream(destination: File, inputStream: InputStream): DataInfo { + return try { + val tempFile = newFile(context) + val messageDigest = MessageDigest.getInstance("SHA-256") + val digestInputStream = DigestInputStream(inputStream, messageDigest) + val out = ModernEncryptingPartOutputStream.createFor(attachmentSecret, tempFile, false) + val length = StreamUtil.copy(digestInputStream, out.second) + val hash = encodeWithPadding(digestInputStream.messageDigest.digest()) + + if (!tempFile.renameTo(destination)) { + Log.w(TAG, "Couldn't rename ${tempFile.path} to ${destination.path}") + tempFile.delete() + throw IllegalStateException("Couldn't rename ${tempFile.path} to ${destination.path}") + } + + DataInfo( + file = destination, + length = length, + random = out.first, + hash = hash, + transformProperties = null + ) + } catch (e: IOException) { + throw MmsException(e) + } catch (e: NoSuchAlgorithmException) { + throw MmsException(e) + } + } + + private fun deduplicateAttachment( + dataInfo: DataInfo, + attachmentId: AttachmentId?, + transformProperties: TransformProperties? + ): DataInfo { + check(writableDatabase.inTransaction()) { "Must be in a transaction!" } + + val sharedDataInfos = findDuplicateDataFileInfos(writableDatabase, dataInfo.hash, attachmentId) + + for (sharedDataInfo in sharedDataInfos) { + if (dataInfo.file == sharedDataInfo.file) { + continue + } + + val isUsedElsewhere = isAttachmentFileUsedByOtherAttachments(attachmentId, dataInfo) + val isSameQuality = transformProperties?.sentMediaQuality == sharedDataInfo.transformProperties?.sentMediaQuality + + Log.i(TAG, "[deduplicateAttachment] Potential duplicate data file found. usedElsewhere: " + isUsedElsewhere + " sameQuality: " + isSameQuality + " otherFile: " + sharedDataInfo.file.absolutePath) + + if (!isSameQuality) { + continue + } + + if (!isUsedElsewhere) { + if (dataInfo.file.delete()) { + Log.i(TAG, "[deduplicateAttachment] Deleted original file. ${dataInfo.file}") + } else { + Log.w(TAG, "[deduplicateAttachment] Original file could not be deleted.") + } + } + + return sharedDataInfo + } + + Log.i(TAG, "[deduplicateAttachment] No acceptable matching attachment data found. ${dataInfo.file.absolutePath}") + return dataInfo + } + + private fun findDuplicateDataFileInfos( + database: SQLiteDatabase, + hash: String, + excludedAttachmentId: AttachmentId? + ): List { + check(database.inTransaction()) { "Must be in a transaction!" } + + val selectorArgs: Pair> = buildSharedFileSelectorArgs(hash, excludedAttachmentId) + + return database + .select(DATA, DATA_RANDOM, SIZE, TRANSFORM_PROPERTIES) + .from(TABLE_NAME) + .where(selectorArgs.first, selectorArgs.second) + .run() + .readToList { cursor -> + DataInfo( + file = File(cursor.requireNonNullString(DATA)), + length = cursor.requireLong(SIZE), + random = cursor.requireNonNullBlob(DATA_RANDOM), + hash = hash, + transformProperties = TransformProperties.parse(cursor.requireString(TRANSFORM_PROPERTIES)) + ) + } + } + + private fun buildSharedFileSelectorArgs(newHash: String, attachmentId: AttachmentId?): Pair> { + return if (attachmentId == null) { + "$DATA_HASH = ?" to arrayOf(newHash) + } else { + "$PART_ID_WHERE_NOT AND $DATA_HASH = ?" to arrayOf( + attachmentId.rowId.toString(), + attachmentId.uniqueId.toString(), + newHash + ) + } + } + + @Throws(MmsException::class) + private fun insertAttachment(mmsId: Long, attachment: Attachment, quote: Boolean): AttachmentId { + Log.d(TAG, "Inserting attachment for mms id: $mmsId") + + var notifyPacks = false + + val attachmentId: AttachmentId = writableDatabase.withinTransaction { db -> + try { + var dataInfo: DataInfo? = null + val uniqueId = System.currentTimeMillis() + + if (attachment.uri != null) { + val storeDataInfo = storeAttachmentStream(PartAuthority.getAttachmentStream(context, attachment.uri!!)) + Log.d(TAG, "Wrote part to file: ${storeDataInfo.file.absolutePath}") + + dataInfo = deduplicateAttachment(storeDataInfo, null, attachment.transformProperties) + } + + var template = attachment + var useTemplateUpload = false + + if (dataInfo != null) { + val possibleTemplates = findTemplateAttachments(dataInfo.hash) + + for (possibleTemplate in possibleTemplates) { + useTemplateUpload = possibleTemplate.uploadTimestamp > attachment.uploadTimestamp && + possibleTemplate.transferState == TRANSFER_PROGRESS_DONE && + possibleTemplate.transformProperties?.shouldSkipTransform() == true && possibleTemplate.digest != null && + attachment.transformProperties?.videoEdited == false && possibleTemplate.transformProperties?.sentMediaQuality == attachment.transformProperties?.sentMediaQuality + + if (useTemplateUpload) { + Log.i(TAG, "Found a duplicate attachment upon insertion. Using it as a template.") + template = possibleTemplate + break + } + } + } + + val contentValues = ContentValues() + contentValues.put(MMS_ID, mmsId) + contentValues.put(CONTENT_TYPE, template.contentType) + contentValues.put(TRANSFER_STATE, attachment.transferState) + contentValues.put(UNIQUE_ID, uniqueId) + contentValues.put(CDN_NUMBER, if (useTemplateUpload) template.cdnNumber else attachment.cdnNumber) + contentValues.put(CONTENT_LOCATION, if (useTemplateUpload) template.location else attachment.location) + contentValues.put(DIGEST, if (useTemplateUpload) template.digest else attachment.digest) + contentValues.put(MAC_DIGEST, if (useTemplateUpload) template.incrementalDigest else attachment.incrementalDigest) + contentValues.put(INCREMENTAL_MAC_CHUNK_SIZE, if (useTemplateUpload) template.incrementalMacChunkSize else attachment.incrementalMacChunkSize) + contentValues.put(CONTENT_DISPOSITION, if (useTemplateUpload) template.key else attachment.key) + contentValues.put(NAME, if (useTemplateUpload) template.relay else attachment.relay) + contentValues.put(FILE_NAME, StorageUtil.getCleanFileName(attachment.fileName)) + contentValues.put(SIZE, template.size) + contentValues.put(FAST_PREFLIGHT_ID, attachment.fastPreflightId) + contentValues.put(VOICE_NOTE, if (attachment.voiceNote) 1 else 0) + contentValues.put(BORDERLESS, if (attachment.borderless) 1 else 0) + contentValues.put(VIDEO_GIF, if (attachment.videoGif) 1 else 0) + contentValues.put(WIDTH, template.width) + contentValues.put(HEIGHT, template.height) + contentValues.put(QUOTE, quote) + contentValues.put(CAPTION, attachment.caption) + contentValues.put(UPLOAD_TIMESTAMP, if (useTemplateUpload) template.uploadTimestamp else attachment.uploadTimestamp) + + if (attachment.transformProperties?.videoEdited == true) { + contentValues.putNull(VISUAL_HASH) + contentValues.put(TRANSFORM_PROPERTIES, attachment.transformProperties?.serialize()) + } else { + contentValues.put(VISUAL_HASH, template.getVisualHashStringOrNull()) + contentValues.put(TRANSFORM_PROPERTIES, (if (useTemplateUpload) template else attachment).transformProperties?.serialize()) + } + + attachment.stickerLocator?.let { sticker -> + contentValues.put(STICKER_PACK_ID, sticker.packId) + contentValues.put(STICKER_PACK_KEY, sticker.packKey) + contentValues.put(STICKER_ID, sticker.stickerId) + contentValues.put(STICKER_EMOJI, sticker.emoji) + } + + if (dataInfo != null) { + contentValues.put(DATA, dataInfo.file.absolutePath) + contentValues.put(SIZE, dataInfo.length) + contentValues.put(DATA_RANDOM, dataInfo.random) + + if (attachment.transformProperties?.videoEdited == true) { + contentValues.putNull(DATA_HASH) + } else { + contentValues.put(DATA_HASH, dataInfo.hash) + } + } + + notifyPacks = attachment.isSticker && !hasStickerAttachments() + + val rowId = db.insert(TABLE_NAME, null, contentValues) + AttachmentId(rowId, uniqueId) + } catch (e: IOException) { + throw MmsException(e) + } + } + + if (notifyPacks) { + notifyStickerPackListeners() + } + + notifyAttachmentListeners() + return attachmentId + } + + private fun findTemplateAttachments(dataHash: String): List { + return readableDatabase + .select(*PROJECTION) + .from(TABLE_NAME) + .where("$DATA_HASH = ?", dataHash) + .run() + .readToList { it.readAttachment() } + } + + private fun getTransferFile(db: SQLiteDatabase, attachmentId: AttachmentId): File? { + return db + .select(TRANSFER_FILE) + .from(TABLE_NAME) + .where(PART_ID_WHERE, attachmentId.toStrings()) + .limit(1) + .run() + .readToSingleObject { cursor -> + cursor.requireString(TRANSFER_FILE)?.let { File(it) } + } + } + + private fun getAttachment(cursor: Cursor): DatabaseAttachment { + val contentType = cursor.requireString(CONTENT_TYPE) + + return DatabaseAttachment( + attachmentId = AttachmentId(cursor.requireLong(ROW_ID), cursor.requireLong(UNIQUE_ID)), + mmsId = cursor.requireLong(MMS_ID), + hasData = !cursor.isNull(DATA), + hasThumbnail = MediaUtil.isImageType(contentType) || MediaUtil.isVideoType(contentType), + contentType = contentType, + transferProgress = cursor.requireInt(TRANSFER_STATE), + size = cursor.requireLong(SIZE), + fileName = cursor.requireString(FILE_NAME), + cdnNumber = cursor.requireInt(CDN_NUMBER), + location = cursor.requireString(CONTENT_LOCATION), + key = cursor.requireString(CONTENT_DISPOSITION), + relay = cursor.requireString(NAME), + digest = cursor.requireBlob(DIGEST), + incrementalDigest = cursor.requireBlob(MAC_DIGEST), + incrementalMacChunkSize = cursor.requireInt(INCREMENTAL_MAC_CHUNK_SIZE), + fastPreflightId = cursor.requireString(FAST_PREFLIGHT_ID), + voiceNote = cursor.requireBoolean(VOICE_NOTE), + borderless = cursor.requireBoolean(BORDERLESS), + videoGif = cursor.requireBoolean(VIDEO_GIF), + width = cursor.requireInt(WIDTH), + height = cursor.requireInt(HEIGHT), + quote = cursor.requireBoolean(QUOTE), + caption = cursor.requireString(CAPTION), + stickerLocator = cursor.readStickerLocator(), + blurHash = if (MediaUtil.isAudioType(contentType)) null else BlurHash.parseOrNull(cursor.requireString(VISUAL_HASH)), + audioHash = if (MediaUtil.isAudioType(contentType)) AudioHash.parseOrNull(cursor.requireString(VISUAL_HASH)) else null, + transformProperties = TransformProperties.parse(cursor.requireString(TRANSFORM_PROPERTIES)), + displayOrder = cursor.requireInt(DISPLAY_ORDER), + uploadTimestamp = cursor.requireLong(UPLOAD_TIMESTAMP) + ) + } + + private fun Cursor.readAttachments(): List { + return getAttachments(this) + } + + private fun Cursor.readAttachment(): DatabaseAttachment { + return getAttachment(this) + } + + private fun Cursor.readStickerLocator(): StickerLocator? { + return if (this.requireInt(STICKER_ID) >= 0) { + StickerLocator( + packId = this.requireNonNullString(STICKER_PACK_ID), + packKey = this.requireNonNullString(STICKER_PACK_KEY), + stickerId = this.requireInt(STICKER_ID), + emoji = this.requireString(STICKER_EMOJI) + ) + } else { + null + } + } + + private fun Attachment?.getVisualHashStringOrNull(): String? { + return when { + this == null -> null + this.blurHash != null -> this.blurHash!!.hash + this.audioHash != null -> this.audioHash!!.hash + else -> null + } + } + + @VisibleForTesting + class DataInfo( + val file: File, + val length: Long, + val random: ByteArray, + val hash: String, + val transformProperties: TransformProperties? + ) { + override fun equals(o: Any?): Boolean { + if (this === o) return true + if (o == null || javaClass != o.javaClass) return false + val dataInfo = o as DataInfo + return length == dataInfo.length && file == dataInfo.file && + Arrays.equals(random, dataInfo.random) && hash == dataInfo.hash && transformProperties == dataInfo.transformProperties + } + + override fun hashCode(): Int { + var result = Objects.hash(file, length, hash, transformProperties) + result = 31 * result + Arrays.hashCode(random) + return result + } + } + + /** + * @param removableWeakReferences Entries in here can be removed from the database. Only possible to be non-empty when [hasStrongReference] is false. + */ + private class DataUsageResult private constructor(val hasStrongReference: Boolean, val removableWeakReferences: List) { + constructor(removableWeakReferences: List) : this(false, removableWeakReferences) + + init { + if (hasStrongReference && removableWeakReferences.isNotEmpty()) { + throw IllegalStateException("There's a strong reference and removable weak references!") + } + } + + companion object { + val IN_USE = DataUsageResult(true, emptyList()) + val NOT_IN_USE = DataUsageResult(false, emptyList()) + } + } + + @Parcelize + data class TransformProperties( + @JsonProperty("skipTransform") + @JvmField + val skipTransform: Boolean, + + @JsonProperty("videoTrim") + @JvmField + val videoTrim: Boolean, + + @JsonProperty("videoTrimStartTimeUs") + @JvmField + val videoTrimStartTimeUs: Long, + + @JsonProperty("videoTrimEndTimeUs") + @JvmField + val videoTrimEndTimeUs: Long, + + @JsonProperty("sentMediaQuality") + @JvmField + val sentMediaQuality: Int, + + @JsonProperty("mp4Faststart") + @JvmField + val mp4FastStart: Boolean + ) : Parcelable { + fun shouldSkipTransform(): Boolean { + return skipTransform + } + + @IgnoredOnParcel + @JsonProperty("videoEdited") + val videoEdited: Boolean = videoTrim + + fun withSkipTransform(): TransformProperties { + return this.copy( + skipTransform = true, + videoTrim = false, + videoTrimStartTimeUs = 0, + videoTrimEndTimeUs = 0, + mp4FastStart = false + ) + } + + fun withMp4FastStart(): TransformProperties { + return this.copy(mp4FastStart = true) + } + + fun serialize(): String { + return JsonUtil.toJson(this) + } + + companion object { + private val DEFAULT_MEDIA_QUALITY = SentMediaQuality.STANDARD.code + + @JvmStatic + fun empty(): TransformProperties { + return TransformProperties( + skipTransform = false, + videoTrim = false, + videoTrimStartTimeUs = 0, + videoTrimEndTimeUs = 0, + sentMediaQuality = DEFAULT_MEDIA_QUALITY, + mp4FastStart = false + ) + } + + fun forSkipTransform(): TransformProperties { + return TransformProperties( + skipTransform = true, + videoTrim = false, + videoTrimStartTimeUs = 0, + videoTrimEndTimeUs = 0, + sentMediaQuality = DEFAULT_MEDIA_QUALITY, + mp4FastStart = false + ) + } + + fun forVideoTrim(videoTrimStartTimeUs: Long, videoTrimEndTimeUs: Long): TransformProperties { + return TransformProperties( + skipTransform = false, + videoTrim = true, + videoTrimStartTimeUs = videoTrimStartTimeUs, + videoTrimEndTimeUs = videoTrimEndTimeUs, + sentMediaQuality = DEFAULT_MEDIA_QUALITY, + mp4FastStart = false + ) + } + + @JvmStatic + fun forSentMediaQuality(currentProperties: Optional, sentMediaQuality: SentMediaQuality): TransformProperties { + val existing = currentProperties.orElse(empty()) + return existing.copy(sentMediaQuality = sentMediaQuality.code) + } + + @JvmStatic + fun parse(serialized: String?): TransformProperties { + return if (serialized == null) { + empty() + } else { + try { + JsonUtil.fromJson(serialized, TransformProperties::class.java) + } catch (e: IOException) { + Log.w(TAG, "Failed to parse TransformProperties!", e) + empty() + } + } + } + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt index cfe5b3b574..d45d7bd17f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt @@ -2316,7 +2316,7 @@ 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 = associatedAttachments.filter { it.isQuote }.toList() + val quoteAttachments: List = associatedAttachments.filter { it.quote }.toList() val quoteMentions: List = parseQuoteMentions(cursor) val quoteBodyRanges: BodyRangeList? = parseQuoteBodyRanges(cursor) val quote: QuoteModel? = if (quoteId != QUOTE_NOT_PRESENT_ID && quoteAuthor > 0 && (!TextUtils.isEmpty(quoteText) || quoteAttachments.isNotEmpty())) { @@ -2330,7 +2330,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat val previews: List = getLinkPreviews(cursor, associatedAttachments) val previewAttachments: Set = previews.filter { it.thumbnail.isPresent }.map { it.thumbnail.get() }.toSet() val attachments: List = associatedAttachments - .filterNot { it.isQuote } + .filterNot { it.quote } .filterNot { contactAttachments.contains(it) } .filterNot { previewAttachments.contains(it) } .sortedWith(DisplayOrderComparator()) @@ -5119,7 +5119,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat val bodyRanges = parseQuoteBodyRanges(cursor) val attachments = attachments.getAttachments(cursor) - val quoteAttachments: List = attachments.filter { it.isQuote } + val quoteAttachments: List = attachments.filter { it.quote } val quoteDeck = SlideDeck(quoteAttachments) return if (quoteId != QUOTE_NOT_PRESENT_ID && quoteAuthor > 0) { @@ -5170,7 +5170,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat @JvmStatic fun buildSlideDeck(attachments: List): SlideDeck { val messageAttachments = attachments - .filterNot { it.isQuote } + .filterNot { it.quote } .sortedWith(DisplayOrderComparator()) return SlideDeck(messageAttachments) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/loaders/GroupedThreadMediaLoader.java b/app/src/main/java/org/thoughtcrime/securesms/database/loaders/GroupedThreadMediaLoader.java index b742701abe..4d4d683e56 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/loaders/GroupedThreadMediaLoader.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/loaders/GroupedThreadMediaLoader.java @@ -182,7 +182,7 @@ public final class GroupedThreadMediaLoader extends AsyncTaskLoader attachments) { Map attachmentIdMap = new HashMap<>(); for (DatabaseAttachment attachment : attachments) { - attachmentIdMap.put(attachment.getAttachmentId(), attachment); + attachmentIdMap.put(attachment.attachmentId, attachment); } List contacts = updateContacts(getSharedContacts(), attachmentIdMap); @@ -369,7 +369,7 @@ public class MmsMessageRecord extends MessageRecord { return null; } - List quoteAttachments = attachments.stream().filter(Attachment::isQuote).collect(Collectors.toList()); + List quoteAttachments = attachments.stream().filter(a -> a.quote).collect(Collectors.toList()); return quote.withAttachment(new SlideDeck(quoteAttachments)); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentCompressionJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentCompressionJob.java index e5ff3861d0..2d5e47e231 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentCompressionJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentCompressionJob.java @@ -37,7 +37,6 @@ import org.thoughtcrime.securesms.util.BitmapDecodingException; import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.ImageCompressionUtil; import org.thoughtcrime.securesms.util.MediaUtil; -import org.thoughtcrime.securesms.util.MemoryFileDescriptor; import org.thoughtcrime.securesms.util.MemoryFileDescriptor.MemoryFileException; import org.thoughtcrime.securesms.video.InMemoryTranscoder; import org.thoughtcrime.securesms.video.StreamingTranscoder; @@ -73,7 +72,7 @@ public final class AttachmentCompressionJob extends BaseJob { boolean mms, int mmsSubscriptionId) { - return new AttachmentCompressionJob(databaseAttachment.getAttachmentId(), + return new AttachmentCompressionJob(databaseAttachment.attachmentId, MediaUtil.isVideo(databaseAttachment) && MediaConstraints.isVideoTranscodeAvailable(), mms, mmsSubscriptionId); @@ -136,13 +135,13 @@ public final class AttachmentCompressionJob extends BaseJob { throw new UndeliverableMessageException("Cannot find the specified attachment."); } - if (databaseAttachment.getTransformProperties().shouldSkipTransform()) { + if (databaseAttachment.transformProperties.shouldSkipTransform()) { Log.i(TAG, "Skipping at the direction of the TransformProperties."); return; } MediaConstraints mediaConstraints = mms ? MediaConstraints.getMmsMediaConstraints(mmsSubscriptionId) - : MediaConstraints.getPushMediaConstraints(SentMediaQuality.fromCode(databaseAttachment.getTransformProperties().getSentMediaQuality())); + : MediaConstraints.getPushMediaConstraints(SentMediaQuality.fromCode(databaseAttachment.transformProperties.sentMediaQuality)); compress(database, mediaConstraints, databaseAttachment); } @@ -194,12 +193,12 @@ public final class AttachmentCompressionJob extends BaseJob { @NonNull TranscoderCancelationSignal cancelationSignal) throws UndeliverableMessageException { - AttachmentTable.TransformProperties transformProperties = attachment.getTransformProperties(); + AttachmentTable.TransformProperties transformProperties = attachment.transformProperties; boolean allowSkipOnFailure = false; if (!MediaConstraints.isVideoTranscodeAvailable()) { - if (transformProperties.isVideoEdited()) { + if (transformProperties.getVideoEdited()) { throw new UndeliverableMessageException("Video edited, but transcode is not available"); } return attachment; @@ -210,15 +209,15 @@ public final class AttachmentCompressionJob extends BaseJob { notification.setIndeterminate(true); } - try (MediaDataSource dataSource = attachmentDatabase.mediaDataSourceFor(attachment.getAttachmentId(), false)) { + try (MediaDataSource dataSource = attachmentDatabase.mediaDataSourceFor(attachment.attachmentId, false)) { if (dataSource == null) { throw new UndeliverableMessageException("Cannot get media data source for attachment."); } - allowSkipOnFailure = !transformProperties.isVideoEdited(); + allowSkipOnFailure = !transformProperties.getVideoEdited(); TranscoderOptions options = null; - if (transformProperties.isVideoTrim()) { - options = new TranscoderOptions(transformProperties.getVideoTrimStartTimeUs(), transformProperties.getVideoTrimEndTimeUs()); + if (transformProperties.videoTrim) { + options = new TranscoderOptions(transformProperties.videoTrimStartTimeUs, transformProperties.videoTrimEndTimeUs); } if (FeatureFlags.useStreamingVideoMuxer()) { @@ -228,7 +227,7 @@ public final class AttachmentCompressionJob extends BaseJob { Log.i(TAG, "Compressing with streaming muxer"); AttachmentSecret attachmentSecret = AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(); - File file = SignalDatabase.attachments().newFile(); + File file = SignalDatabase.attachments().newFile(context); file.deleteOnExit(); try { @@ -258,9 +257,9 @@ public final class AttachmentCompressionJob extends BaseJob { } } - attachmentDatabase.markAttachmentAsTransformed(attachment.getAttachmentId(), false); + attachmentDatabase.markAttachmentAsTransformed(attachment.attachmentId, false); - return Objects.requireNonNull(attachmentDatabase.getAttachment(attachment.getAttachmentId())); + return Objects.requireNonNull(attachmentDatabase.getAttachment(attachment.attachmentId)); } else { Log.i(TAG, "Transcode was not required"); } @@ -279,14 +278,14 @@ public final class AttachmentCompressionJob extends BaseJob { percent)); }, cancelationSignal)) { attachmentDatabase.updateAttachmentData(attachment, mediaStream, true); - attachmentDatabase.markAttachmentAsTransformed(attachment.getAttachmentId(), mediaStream.getFaststart()); + attachmentDatabase.markAttachmentAsTransformed(attachment.attachmentId, mediaStream.getFaststart()); } eventBus.postSticky(new PartProgressEvent(attachment, PartProgressEvent.Type.COMPRESSION, 100, 100)); - return Objects.requireNonNull(attachmentDatabase.getAttachment(attachment.getAttachmentId())); + return Objects.requireNonNull(attachmentDatabase.getAttachment(attachment.attachmentId)); } else { Log.i(TAG, "Transcode was not required (in-memory transcoder)"); } @@ -294,7 +293,7 @@ public final class AttachmentCompressionJob extends BaseJob { } } } catch (VideoSourceException | EncodingException | MemoryFileException e) { - if (attachment.getSize() > constraints.getVideoMaxSize(context)) { + if (attachment.size > constraints.getVideoMaxSize(context)) { throw new UndeliverableMessageException("Duration not found, attachment too large to skip transcode", e); } else { if (allowSkipOnFailure) { @@ -330,7 +329,7 @@ public final class AttachmentCompressionJob extends BaseJob { try { for (int size : mediaConstraints.getImageDimensionTargets(context)) { result = ImageCompressionUtil.compressWithinConstraints(context, - attachment.getContentType(), + attachment.contentType, new DecryptableStreamUriLoader.DecryptableUri(uri), size, mediaConstraints.getImageMaxSize(context), diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java index c4414daee5..bc7c37e6b6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java @@ -119,8 +119,8 @@ public final class AttachmentDownloadJob extends BaseJob { final AttachmentTable database = SignalDatabase.attachments(); final AttachmentId attachmentId = new AttachmentId(partRowId, partUniqueId); final DatabaseAttachment attachment = database.getAttachment(attachmentId); - final boolean pending = attachment != null && attachment.getTransferState() != AttachmentTable.TRANSFER_PROGRESS_DONE - && attachment.getTransferState() != AttachmentTable.TRANSFER_PROGRESS_PERMANENT_FAILURE; + final boolean pending = attachment != null && attachment.transferState != AttachmentTable.TRANSFER_PROGRESS_DONE + && attachment.transferState != AttachmentTable.TRANSFER_PROGRESS_PERMANENT_FAILURE; if (pending && (manual || AttachmentUtil.isAutoDownloadPermitted(context, attachment))) { Log.i(TAG, "onAdded() Marking attachment progress as 'started'"); @@ -168,7 +168,7 @@ public final class AttachmentDownloadJob extends BaseJob { Log.i(TAG, "Downloading push part " + attachmentId); database.setTransferState(messageId, attachmentId, AttachmentTable.TRANSFER_PROGRESS_STARTED); - if (attachment.getCdnNumber() != ReleaseChannel.CDN_NUMBER) { + if (attachment.cdnNumber != ReleaseChannel.CDN_NUMBER) { retrieveAttachment(messageId, attachmentId, attachment); } else { retrieveUrlAttachment(messageId, attachmentId, attachment); @@ -200,7 +200,7 @@ public final class AttachmentDownloadJob extends BaseJob { File attachmentFile = database.getOrCreateTransferFile(attachmentId); try { - if (attachment.getSize() > maxReceiveSize) { + if (attachment.size > maxReceiveSize) { throw new MmsException("Attachment too large, failing download"); } SignalServiceMessageReceiver messageReceiver = ApplicationDependencies.getSignalServiceMessageReceiver(); @@ -243,38 +243,38 @@ public final class AttachmentDownloadJob extends BaseJob { } private SignalServiceAttachmentPointer createAttachmentPointer(Attachment attachment) throws InvalidPartException { - if (TextUtils.isEmpty(attachment.getLocation())) { + if (TextUtils.isEmpty(attachment.location)) { throw new InvalidPartException("empty content id"); } - if (TextUtils.isEmpty(attachment.getKey())) { + if (TextUtils.isEmpty(attachment.key)) { throw new InvalidPartException("empty encrypted key"); } try { - final SignalServiceAttachmentRemoteId remoteId = SignalServiceAttachmentRemoteId.from(attachment.getLocation()); - final byte[] key = Base64.decode(attachment.getKey()); + final SignalServiceAttachmentRemoteId remoteId = SignalServiceAttachmentRemoteId.from(attachment.location); + final byte[] key = Base64.decode(attachment.key); - if (attachment.getDigest() != null) { - Log.i(TAG, "Downloading attachment with digest: " + Hex.toString(attachment.getDigest())); + if (attachment.digest != null) { + Log.i(TAG, "Downloading attachment with digest: " + Hex.toString(attachment.digest)); } else { Log.i(TAG, "Downloading attachment with no digest..."); } - return new SignalServiceAttachmentPointer(attachment.getCdnNumber(), remoteId, null, key, - Optional.of(Util.toIntExact(attachment.getSize())), + return new SignalServiceAttachmentPointer(attachment.cdnNumber, remoteId, null, key, + Optional.of(Util.toIntExact(attachment.size)), Optional.empty(), 0, 0, - Optional.ofNullable(attachment.getDigest()), + Optional.ofNullable(attachment.digest), Optional.ofNullable(attachment.getIncrementalDigest()), - attachment.getIncrementalMacChunkSize(), - Optional.ofNullable(attachment.getFileName()), - attachment.isVoiceNote(), - attachment.isBorderless(), - attachment.isVideoGif(), + attachment.incrementalMacChunkSize, + Optional.ofNullable(attachment.fileName), + attachment.voiceNote, + attachment.borderless, + attachment.videoGif, Optional.empty(), - Optional.ofNullable(attachment.getBlurHash()).map(BlurHash::getHash), - attachment.getUploadTimestamp()); + Optional.ofNullable(attachment.blurHash).map(BlurHash::getHash), + attachment.uploadTimestamp); } catch (IOException | ArithmeticException e) { Log.w(TAG, e); throw new InvalidPartException(e); @@ -286,7 +286,7 @@ public final class AttachmentDownloadJob extends BaseJob { final Attachment attachment) throws IOException { - try (Response response = S3.getObject(Objects.requireNonNull(attachment.getFileName()))) { + try (Response response = S3.getObject(Objects.requireNonNull(attachment.fileName))) { ResponseBody body = response.body(); if (body != null) { if (body.contentLength() > FeatureFlags.maxAttachmentReceiveSizeBytes()) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentUploadJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentUploadJob.kt index fd619bd96c..48438f351d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentUploadJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentUploadJob.kt @@ -174,10 +174,10 @@ class AttachmentUploadJob private constructor( .withContentType(attachment.contentType) .withLength(attachment.size) .withFileName(attachment.fileName) - .withVoiceNote(attachment.isVoiceNote) - .withBorderless(attachment.isBorderless) - .withGif(attachment.isVideoGif) - .withFaststart(attachment.transformProperties.isMp4Faststart) + .withVoiceNote(attachment.voiceNote) + .withBorderless(attachment.borderless) + .withGif(attachment.videoGif) + .withFaststart(attachment.transformProperties?.mp4FastStart ?: false) .withWidth(attachment.width) .withHeight(attachment.height) .withUploadTimestamp(System.currentTimeMillis()) diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/LegacyAttachmentUploadJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/LegacyAttachmentUploadJob.java index 027ef5896c..3e12bb822e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/LegacyAttachmentUploadJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/LegacyAttachmentUploadJob.java @@ -126,22 +126,22 @@ public final class LegacyAttachmentUploadJob extends BaseJob { throw new InvalidAttachmentException("Cannot find the specified attachment."); } - long timeSinceUpload = System.currentTimeMillis() - databaseAttachment.getUploadTimestamp(); - if (timeSinceUpload < UPLOAD_REUSE_THRESHOLD && !TextUtils.isEmpty(databaseAttachment.getLocation())) { + long timeSinceUpload = System.currentTimeMillis() - databaseAttachment.uploadTimestamp; + if (timeSinceUpload < UPLOAD_REUSE_THRESHOLD && !TextUtils.isEmpty(databaseAttachment.location)) { Log.i(TAG, "We can re-use an already-uploaded file. It was uploaded " + timeSinceUpload + " ms ago. Skipping."); return; - } else if (databaseAttachment.getUploadTimestamp() > 0) { + } else if (databaseAttachment.uploadTimestamp > 0) { Log.i(TAG, "This file was previously-uploaded, but too long ago to be re-used. Age: " + timeSinceUpload + " ms"); } - Log.i(TAG, "Uploading attachment for message " + databaseAttachment.getMmsId() + " with ID " + databaseAttachment.getAttachmentId()); + Log.i(TAG, "Uploading attachment for message " + databaseAttachment.mmsId + " with ID " + databaseAttachment.attachmentId); try (NotificationController notification = getNotificationForAttachment(databaseAttachment)) { try (SignalServiceAttachmentStream localAttachment = getAttachmentFor(databaseAttachment, notification, resumableUploadSpec)) { SignalServiceAttachmentPointer remoteAttachment = messageSender.uploadAttachment(localAttachment); - Attachment attachment = PointerAttachment.forPointer(Optional.of(remoteAttachment), null, databaseAttachment.getFastPreflightId()).get(); + Attachment attachment = PointerAttachment.forPointer(Optional.of(remoteAttachment), null, databaseAttachment.fastPreflightId).get(); - database.updateAttachmentAfterUpload(databaseAttachment.getAttachmentId(), attachment, remoteAttachment.getUploadTimestamp()); + database.updateAttachmentAfterUpload(databaseAttachment.attachmentId, attachment, remoteAttachment.getUploadTimestamp()); } } catch (NonSuccessfulResumableUploadResponseCodeException e) { if (e.getCode() == 400) { @@ -152,7 +152,7 @@ public final class LegacyAttachmentUploadJob extends BaseJob { } private @Nullable NotificationController getNotificationForAttachment(@NonNull Attachment attachment) { - if (attachment.getSize() >= FOREGROUND_LIMIT) { + if (attachment.size >= FOREGROUND_LIMIT) { try { return ForegroundServiceUtil.startGenericTaskWhenCapable(context, context.getString(R.string.AttachmentUploadJob_uploading_media)); } catch (UnableToStartException e) { @@ -179,7 +179,7 @@ public final class LegacyAttachmentUploadJob extends BaseJob { } private @NonNull SignalServiceAttachmentStream getAttachmentFor(Attachment attachment, @Nullable NotificationController notification, @Nullable ResumableUploadSpec resumableUploadSpec) throws InvalidAttachmentException { - if (attachment.getUri() == null || attachment.getSize() == 0) { + if (attachment.getUri() == null || attachment.size == 0) { throw new InvalidAttachmentException(new IOException("Assertion failed, outgoing attachment has no data!")); } @@ -187,17 +187,17 @@ public final class LegacyAttachmentUploadJob extends BaseJob { InputStream is = PartAuthority.getAttachmentStream(context, attachment.getUri()); SignalServiceAttachment.Builder builder = SignalServiceAttachment.newStreamBuilder() .withStream(is) - .withContentType(attachment.getContentType()) - .withLength(attachment.getSize()) - .withFileName(attachment.getFileName()) - .withVoiceNote(attachment.isVoiceNote()) - .withBorderless(attachment.isBorderless()) - .withGif(attachment.isVideoGif()) - .withFaststart(attachment.getTransformProperties().isMp4Faststart()) - .withWidth(attachment.getWidth()) - .withHeight(attachment.getHeight()) + .withContentType(attachment.contentType) + .withLength(attachment.size) + .withFileName(attachment.fileName) + .withVoiceNote(attachment.voiceNote) + .withBorderless(attachment.borderless) + .withGif(attachment.videoGif) + .withFaststart(attachment.transformProperties.mp4FastStart) + .withWidth(attachment.width) + .withHeight(attachment.height) .withUploadTimestamp(System.currentTimeMillis()) - .withCaption(attachment.getCaption()) + .withCaption(attachment.caption) .withCancelationSignal(this::isCanceled) .withResumableUploadSpec(resumableUploadSpec) .withListener(new SignalServiceAttachment.ProgressListener() { @@ -214,9 +214,9 @@ public final class LegacyAttachmentUploadJob extends BaseJob { return isCanceled(); } }); - if (MediaUtil.isImageType(attachment.getContentType())) { + if (MediaUtil.isImageType(attachment.contentType)) { return builder.withBlurHash(getImageBlurHash(attachment)).build(); - } else if (MediaUtil.isVideoType(attachment.getContentType())) { + } else if (MediaUtil.isVideoType(attachment.contentType)) { return builder.withBlurHash(getVideoBlurHash(attachment)).build(); } else { return builder.build(); @@ -227,7 +227,7 @@ public final class LegacyAttachmentUploadJob extends BaseJob { } private @Nullable String getImageBlurHash(@NonNull Attachment attachment) throws IOException { - if (attachment.getBlurHash() != null) return attachment.getBlurHash().getHash(); + if (attachment.blurHash != null) return attachment.blurHash.getHash(); if (attachment.getUri() == null) return null; try (InputStream inputStream = PartAuthority.getAttachmentStream(context, attachment.getUri())) { @@ -236,8 +236,8 @@ public final class LegacyAttachmentUploadJob extends BaseJob { } private @Nullable String getVideoBlurHash(@NonNull Attachment attachment) throws IOException { - if (attachment.getBlurHash() != null) { - return attachment.getBlurHash().getHash(); + if (attachment.blurHash != null) { + return attachment.blurHash.getHash(); } if (Build.VERSION.SDK_INT < 23) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MmsSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/MmsSendJob.java index b564850518..80ce82a0b3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/MmsSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MmsSendJob.java @@ -285,21 +285,21 @@ public final class MmsSendJob extends SendJob { try { if (attachment.getUri() == null) throw new IOException("Assertion failed, attachment for outgoing MMS has no data!"); - String fileName = attachment.getFileName(); + String fileName = attachment.fileName; PduPart part = new PduPart(); if (fileName == null) { fileName = String.valueOf(Math.abs(new SecureRandom().nextLong())); - String fileExtension = MimeTypeMap.getSingleton().getExtensionFromMimeType(attachment.getContentType()); + String fileExtension = MimeTypeMap.getSingleton().getExtensionFromMimeType(attachment.contentType); if (fileExtension != null) fileName = fileName + "." + fileExtension; } - if (attachment.getContentType().startsWith("text")) { + if (attachment.contentType.startsWith("text")) { part.setCharset(CharacterSets.UTF_8); } - part.setContentType(attachment.getContentType().getBytes()); + part.setContentType(attachment.contentType.getBytes()); part.setContentLocation(fileName.getBytes()); part.setName(fileName.getBytes()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDistributionListSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDistributionListSendJob.java index 23eae8b92e..c1a5ed9d56 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDistributionListSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDistributionListSendJob.java @@ -106,7 +106,7 @@ public final class PushDistributionListSendJob extends PushSendJob { if (!message.getStoryType().isTextStory()) { DatabaseAttachment storyAttachment = (DatabaseAttachment) message.getAttachments().get(0); - SignalDatabase.attachments().updateAttachmentCaption(storyAttachment.getAttachmentId(), message.getBody()); + SignalDatabase.attachments().updateAttachmentCaption(storyAttachment.attachmentId, message.getBody()); } Set attachmentUploadIds = enqueueCompressingAndUploadAttachmentsChains(jobManager, message); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java index 899665857e..a1fde86a7d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java @@ -197,20 +197,20 @@ public abstract class PushSendJob extends SendJob { protected SignalServiceAttachment getAttachmentFor(Attachment attachment) { try { - if (attachment.getUri() == null || attachment.getSize() == 0) throw new IOException("Assertion failed, outgoing attachment has no data!"); + if (attachment.getUri() == null || attachment.size == 0) throw new IOException("Assertion failed, outgoing attachment has no data!"); InputStream is = PartAuthority.getAttachmentStream(context, attachment.getUri()); return SignalServiceAttachment.newStreamBuilder() .withStream(is) - .withContentType(attachment.getContentType()) - .withLength(attachment.getSize()) - .withFileName(attachment.getFileName()) - .withVoiceNote(attachment.isVoiceNote()) - .withBorderless(attachment.isBorderless()) - .withGif(attachment.isVideoGif()) - .withFaststart(attachment.getTransformProperties().isMp4Faststart()) - .withWidth(attachment.getWidth()) - .withHeight(attachment.getHeight()) - .withCaption(attachment.getCaption()) + .withContentType(attachment.contentType) + .withLength(attachment.size) + .withFileName(attachment.fileName) + .withVoiceNote(attachment.voiceNote) + .withBorderless(attachment.borderless) + .withGif(attachment.videoGif) + .withFaststart(attachment.transformProperties.mp4FastStart) + .withWidth(attachment.width) + .withHeight(attachment.height) + .withCaption(attachment.caption) .withListener(new SignalServiceAttachment.ProgressListener() { @Override public void onAttachmentProgress(long total, long progress) { @@ -246,7 +246,7 @@ public abstract class PushSendJob extends SendJob { .toList()); return new HashSet<>(Stream.of(attachments).map(a -> { - AttachmentUploadJob attachmentUploadJob = new AttachmentUploadJob(((DatabaseAttachment) a).getAttachmentId()); + AttachmentUploadJob attachmentUploadJob = new AttachmentUploadJob(((DatabaseAttachment) a).attachmentId); jobManager.startChain(AttachmentCompressionJob.fromAttachment((DatabaseAttachment) a, false, -1)) .then(attachmentUploadJob) @@ -262,22 +262,22 @@ public abstract class PushSendJob extends SendJob { } protected @Nullable SignalServiceAttachment getAttachmentPointerFor(Attachment attachment) { - if (TextUtils.isEmpty(attachment.getLocation())) { + if (TextUtils.isEmpty(attachment.location)) { Log.w(TAG, "empty content id"); return null; } - if (TextUtils.isEmpty(attachment.getKey())) { + if (TextUtils.isEmpty(attachment.key)) { Log.w(TAG, "empty encrypted key"); return null; } try { - final SignalServiceAttachmentRemoteId remoteId = SignalServiceAttachmentRemoteId.from(attachment.getLocation()); - final byte[] key = Base64.decode(attachment.getKey()); + final SignalServiceAttachmentRemoteId remoteId = SignalServiceAttachmentRemoteId.from(attachment.location); + final byte[] key = Base64.decode(attachment.key); - int width = attachment.getWidth(); - int height = attachment.getHeight(); + int width = attachment.width; + int height = attachment.height; if ((width == 0 || height == 0) && MediaUtil.hasVideoThumbnail(context, attachment.getUri())) { Bitmap thumbnail = MediaUtil.getVideoThumbnail(context, attachment.getUri(), 1000); @@ -288,24 +288,24 @@ public abstract class PushSendJob extends SendJob { } } - return new SignalServiceAttachmentPointer(attachment.getCdnNumber(), + return new SignalServiceAttachmentPointer(attachment.cdnNumber, remoteId, - attachment.getContentType(), + attachment.contentType, key, - Optional.of(Util.toIntExact(attachment.getSize())), + Optional.of(Util.toIntExact(attachment.size)), Optional.empty(), width, height, - Optional.ofNullable(attachment.getDigest()), + Optional.ofNullable(attachment.digest), Optional.ofNullable(attachment.getIncrementalDigest()), - attachment.getIncrementalMacChunkSize(), - Optional.ofNullable(attachment.getFileName()), - attachment.isVoiceNote(), - attachment.isBorderless(), - attachment.isVideoGif(), - Optional.ofNullable(attachment.getCaption()), - Optional.ofNullable(attachment.getBlurHash()).map(BlurHash::getHash), - attachment.getUploadTimestamp()); + attachment.incrementalMacChunkSize, + Optional.ofNullable(attachment.fileName), + attachment.voiceNote, + attachment.borderless, + attachment.videoGif, + Optional.ofNullable(attachment.caption), + Optional.ofNullable(attachment.blurHash).map(BlurHash::getHash), + attachment.uploadTimestamp); } catch (IOException | ArithmeticException e) { Log.w(TAG, e); return null; @@ -353,7 +353,7 @@ public abstract class PushSendJob extends SendJob { Optional localQuoteAttachment = message.getOutgoingQuote() .getAttachments() .stream() - .filter(a -> !MediaUtil.isViewOnceType(a.getContentType())) + .filter(a -> !MediaUtil.isViewOnceType(a.contentType)) .findFirst(); if (localQuoteAttachment.isPresent()) { @@ -363,13 +363,13 @@ public abstract class PushSendJob extends SendJob { SignalServiceAttachment thumbnail = null; try { - if (MediaUtil.isImageType(attachment.getContentType()) && attachment.getUri() != null) { - thumbnailData = ImageCompressionUtil.compress(context, attachment.getContentType(), new DecryptableUri(attachment.getUri()), 100, 50); - } else if (Build.VERSION.SDK_INT >= 23 && MediaUtil.isVideoType(attachment.getContentType()) && attachment.getUri() != null) { + if (MediaUtil.isImageType(attachment.contentType) && attachment.getUri() != null) { + thumbnailData = ImageCompressionUtil.compress(context, attachment.contentType, new DecryptableUri(attachment.getUri()), 100, 50); + } else if (Build.VERSION.SDK_INT >= 23 && MediaUtil.isVideoType(attachment.contentType) && attachment.getUri() != null) { Bitmap bitmap = MediaUtil.getVideoThumbnail(context, attachment.getUri(), 1000); if (bitmap != null) { - thumbnailData = ImageCompressionUtil.compress(context, attachment.getContentType(), new DecryptableUri(attachment.getUri()), 100, 50); + thumbnailData = ImageCompressionUtil.compress(context, attachment.contentType, new DecryptableUri(attachment.getUri()), 100, 50); } } @@ -385,8 +385,8 @@ public abstract class PushSendJob extends SendJob { thumbnail = builder.build(); } - quoteAttachments.add(new SignalServiceDataMessage.Quote.QuotedAttachment(attachment.isVideoGif() ? MediaUtil.IMAGE_GIF : attachment.getContentType(), - attachment.getFileName(), + quoteAttachments.add(new SignalServiceDataMessage.Quote.QuotedAttachment(attachment.videoGif ? MediaUtil.IMAGE_GIF : attachment.contentType, + attachment.fileName, thumbnail)); } catch (BitmapDecodingException e) { Log.w(TAG, e); @@ -412,10 +412,10 @@ public abstract class PushSendJob extends SendJob { } try { - byte[] packId = Hex.fromStringCondensed(stickerAttachment.getSticker().getPackId()); - byte[] packKey = Hex.fromStringCondensed(stickerAttachment.getSticker().getPackKey()); - int stickerId = stickerAttachment.getSticker().getStickerId(); - StickerRecord record = SignalDatabase.stickers().getSticker(stickerAttachment.getSticker().getPackId(), stickerId, false); + byte[] packId = Hex.fromStringCondensed(stickerAttachment.stickerLocator.packId); + byte[] packKey = Hex.fromStringCondensed(stickerAttachment.stickerLocator.packKey); + int stickerId = stickerAttachment.stickerLocator.stickerId; + StickerRecord record = SignalDatabase.stickers().getSticker(stickerAttachment.stickerLocator.packId, stickerId, false); String emoji = record != null ? record.getEmoji() : null; SignalServiceAttachment attachment = getAttachmentPointerFor(stickerAttachment); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/SendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/SendJob.java index 8214328b0d..659a71b8ab 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/SendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/SendJob.java @@ -66,7 +66,7 @@ public abstract class SendJob extends BaseJob { protected String buildAttachmentString(@NonNull List attachments) { List strings = attachments.stream().map(attachment -> { if (attachment instanceof DatabaseAttachment) { - return ((DatabaseAttachment) attachment).getAttachmentId().toString(); + return ((DatabaseAttachment) attachment).attachmentId.toString(); } else if (attachment.getUri() != null) { return attachment.getUri().toString(); } else { diff --git a/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreview.java b/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreview.java index 527752c9ec..67b5340d20 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreview.java +++ b/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreview.java @@ -45,7 +45,7 @@ public class LinkPreview implements Parcelable { this.description = description; this.date = date; this.thumbnail = Optional.of(thumbnail); - this.attachmentId = thumbnail.getAttachmentId(); + this.attachmentId = thumbnail.attachmentId; } public LinkPreview(@NonNull String url, @NonNull String title, @NonNull String description, long date, @NonNull Optional thumbnail) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediaoverview/MediaActions.java b/app/src/main/java/org/thoughtcrime/securesms/mediaoverview/MediaActions.java index 5608d89273..b24d95dcb2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediaoverview/MediaActions.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediaoverview/MediaActions.java @@ -101,7 +101,7 @@ final class MediaActions { attachments.add(new SaveAttachmentTask.Attachment(mediaRecord.getAttachment().getUri(), mediaRecord.getContentType(), mediaRecord.getDate(), - mediaRecord.getAttachment().getFileName())); + mediaRecord.getAttachment().fileName)); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediaoverview/MediaGalleryAllAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/mediaoverview/MediaGalleryAllAdapter.java index 9df8e2f3d5..d3bb27fde0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediaoverview/MediaGalleryAllAdapter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediaoverview/MediaGalleryAllAdapter.java @@ -203,7 +203,7 @@ final class MediaGalleryAllAdapter extends StickyHeaderGridAdapter { } public void toggleSelection(@NonNull MediaRecord mediaRecord) { - AttachmentId attachmentId = mediaRecord.getAttachment().getAttachmentId(); + AttachmentId attachmentId = mediaRecord.getAttachment().attachmentId; MediaTable.MediaRecord removed = selected.remove(attachmentId); if (removed == null) { selected.put(attachmentId, mediaRecord); @@ -219,7 +219,7 @@ final class MediaGalleryAllAdapter extends StickyHeaderGridAdapter { public long getSelectedMediaTotalFileSize() { //noinspection ConstantConditions attacment cannot be null if selected return Stream.of(selected.values()) - .collect(Collectors.summingLong(a -> a.getAttachment().getSize())); + .collect(Collectors.summingLong(a -> a.getAttachment().size)); } @NonNull @@ -238,7 +238,7 @@ final class MediaGalleryAllAdapter extends StickyHeaderGridAdapter { int sectionItemCount = media.getSectionItemCount(section); for (int item = 0; item < sectionItemCount; item++) { MediaRecord mediaRecord = media.get(section, item); - selected.put(mediaRecord.getAttachment().getAttachmentId(), mediaRecord); + selected.put(mediaRecord.getAttachment().attachmentId, mediaRecord); } } this.notifyItemRangeChanged(0, getItemCount(), PAYLOAD_SELECTED); @@ -282,7 +282,7 @@ final class MediaGalleryAllAdapter extends StickyHeaderGridAdapter { } protected boolean isSelected() { - return selected.containsKey(mediaRecord.getAttachment().getAttachmentId()); + return selected.containsKey(mediaRecord.getAttachment().attachmentId); } protected void updateSelectedView() { @@ -539,11 +539,11 @@ final class MediaGalleryAllAdapter extends StickyHeaderGridAdapter { throw new AssertionError(); } - isVoiceNote = slide.asAttachment().isVoiceNote(); + isVoiceNote = slide.asAttachment().voiceNote; super.bind(context, mediaRecord, slide); - long mmsId = Objects.requireNonNull(mediaRecord.getAttachment()).getMmsId(); + long mmsId = Objects.requireNonNull(mediaRecord.getAttachment()).mmsId; audioItemListener.unregisterPlaybackStateObserver(audioView.getPlaybackStateObserver()); audioView.setAudio((AudioSlide) slide, new AudioViewCallbacksAdapter(audioItemListener, mmsId), true, true); diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediaoverview/MediaOverviewPageFragment.java b/app/src/main/java/org/thoughtcrime/securesms/mediaoverview/MediaOverviewPageFragment.java index 92c1c6ee4c..9ef52f7d5b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediaoverview/MediaOverviewPageFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediaoverview/MediaOverviewPageFragment.java @@ -247,17 +247,17 @@ public final class MediaOverviewPageFragment extends Fragment mediaRecord.getDate(), Objects.requireNonNull(mediaRecord.getAttachment().getUri()), mediaRecord.getContentType(), - mediaRecord.getAttachment().getSize(), - mediaRecord.getAttachment().getCaption(), + mediaRecord.getAttachment().size, + mediaRecord.getAttachment().caption, true, true, threadId == MediaTable.ALL_THREADS, true, sorting, - attachment.isVideoGif(), + attachment.videoGif, new MediaIntentFactory.SharedElementArgs( - attachment.getWidth(), - attachment.getHeight(), + attachment.width, + attachment.height, DimensionUnit.DP.toDp(12), DimensionUnit.DP.toDp(12), DimensionUnit.DP.toDp(12), diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaIntentFactory.kt b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaIntentFactory.kt index 76c68f3ba5..d33c4552c6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaIntentFactory.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaIntentFactory.kt @@ -77,7 +77,7 @@ object MediaIntentFactory { leftIsRecent, allMediaInRail = allMediaInRail, sorting = MediaTable.Sorting.Newest, - isVideoGif = attachment.isVideoGif, + isVideoGif = attachment.videoGif, sharedElementArgs = SharedElementArgs( attachment.width, attachment.height, diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2Adapter.kt b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2Adapter.kt index 5803925a4d..5ad24e4c72 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2Adapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2Adapter.kt @@ -31,8 +31,8 @@ class MediaPreviewV2Adapter(fragment: Fragment) : FragmentStateAdapter(fragment) MediaPreviewFragment.DATA_URI to attachment.uri, MediaPreviewFragment.DATA_CONTENT_TYPE to contentType, MediaPreviewFragment.DATA_SIZE to attachment.size, - MediaPreviewFragment.AUTO_PLAY to attachment.isVideoGif, - MediaPreviewFragment.VIDEO_GIF to attachment.isVideoGif + MediaPreviewFragment.AUTO_PLAY to attachment.videoGif, + MediaPreviewFragment.VIDEO_GIF to attachment.videoGif ) val fragment = if (MediaUtil.isVideo(contentType)) { VideoMediaPreviewFragment() diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2Fragment.kt b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2Fragment.kt index 6379b5640e..80130dda44 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2Fragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2Fragment.kt @@ -331,7 +331,7 @@ class MediaPreviewV2Fragment : LoggingFragment(R.layout.fragment_media_preview_v } private fun bindMediaPreviewPlaybackControls(currentItem: MediaTable.MediaRecord, currentFragment: MediaPreviewFragment?) { - val mediaType: MediaPreviewPlayerControlView.MediaMode = if (currentItem.attachment?.isVideoGif == true) { + val mediaType: MediaPreviewPlayerControlView.MediaMode = if (currentItem.attachment?.videoGif == true) { MediaPreviewPlayerControlView.MediaMode.IMAGE } else { MediaPreviewPlayerControlView.MediaMode.fromString(currentItem.contentType) diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2ViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2ViewModel.kt index 3632ace6d3..e41f7a08a8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2ViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2ViewModel.kt @@ -127,8 +127,8 @@ fun MediaTable.MediaRecord.toMedia(): Media? { attachment.height, attachment.size, 0, - attachment.isBorderless, - attachment.isVideoGif, + attachment.borderless, + attachment.videoGif, Optional.empty(), Optional.ofNullable(attachment.caption), Optional.empty() diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaUploadRepository.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaUploadRepository.java index 7bb5a47fde..45983941f8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaUploadRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaUploadRepository.java @@ -100,7 +100,7 @@ public class MediaUploadRepository { return oldProperties == newProperties; } - return !newProperties.isVideoEdited() && oldProperties.getSentMediaQuality() == newProperties.getSentMediaQuality(); + return !newProperties.getVideoEdited() && oldProperties.sentMediaQuality == newProperties.sentMediaQuality; } public void cancelUpload(@NonNull Media media) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionRepository.kt index 4fde5ae4d0..fa2cd294c0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionRepository.kt @@ -109,7 +109,7 @@ class MediaSelectionRepository(context: Context) { val updatedMedia = oldToNewMediaMap.values.toList() for (media in updatedMedia) { - Log.w(TAG, media.uri.toString() + " : " + media.transformProperties.map { t: TransformProperties -> "" + t.isVideoTrim }.orElse("null")) + Log.w(TAG, media.uri.toString() + " : " + media.transformProperties.map { t: TransformProperties -> "" + t.videoTrim }.orElse("null")) } val singleRecipient: Recipient? = singleContact?.let { Recipient.resolved(it.recipientId) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/migrations/LegacyMigrationJob.java b/app/src/main/java/org/thoughtcrime/securesms/migrations/LegacyMigrationJob.java index fbdb1c3263..e4d286e31f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/migrations/LegacyMigrationJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/migrations/LegacyMigrationJob.java @@ -246,15 +246,15 @@ public class LegacyMigrationJob extends MigrationJob { Log.i(TAG, pendingAttachments.size() + " pending parts."); for (DatabaseAttachment attachment : pendingAttachments) { - final MmsReader reader = MessageTable.mmsReaderFor(mmsDb.getMessageCursor(attachment.getMmsId())); + final MmsReader reader = MessageTable.mmsReaderFor(mmsDb.getMessageCursor(attachment.mmsId)); final MessageRecord record = reader.getNext(); - if (attachment.hasData()) { - Log.i(TAG, "corrected a pending media part " + attachment.getAttachmentId() + "that already had data."); - attachmentDb.setTransferState(attachment.getMmsId(), attachment.getAttachmentId(), AttachmentTable.TRANSFER_PROGRESS_DONE); + if (attachment.hasData) { + Log.i(TAG, "corrected a pending media part " + attachment.attachmentId + "that already had data."); + attachmentDb.setTransferState(attachment.mmsId, attachment.attachmentId, AttachmentTable.TRANSFER_PROGRESS_DONE); } else if (record != null && !record.isOutgoing() && record.isPush()) { - Log.i(TAG, "queuing new attachment download job for incoming push part " + attachment.getAttachmentId() + "."); - ApplicationDependencies.getJobManager().add(new AttachmentDownloadJob(attachment.getMmsId(), attachment.getAttachmentId(), false)); + Log.i(TAG, "queuing new attachment download job for incoming push part " + attachment.attachmentId + "."); + ApplicationDependencies.getJobManager().add(new AttachmentDownloadJob(attachment.mmsId, attachment.attachmentId, false)); } reader.close(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/AttachmentManager.java b/app/src/main/java/org/thoughtcrime/securesms/mms/AttachmentManager.java index d90f6b8266..e7727f719c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/AttachmentManager.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/AttachmentManager.java @@ -323,7 +323,7 @@ public class AttachmentManager { result.set(true); } else { Attachment attachment = slide.asAttachment(); - result.deferTo(thumbnail.setImageResource(glideRequests, slide, false, true, attachment.getWidth(), attachment.getHeight())); + result.deferTo(thumbnail.setImageResource(glideRequests, slide, false, true, attachment.width, attachment.height)); removableMediaView.display(thumbnail, mediaType == SlideFactory.MediaType.IMAGE); } @@ -546,7 +546,7 @@ public class AttachmentManager { MediaIntentFactory.UNKNOWN_TIMESTAMP, slide.getUri(), slide.getContentType(), - slide.asAttachment().getSize(), + slide.asAttachment().size, slide.getCaption().orElse(null), false, false, diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/GifSlide.java b/app/src/main/java/org/thoughtcrime/securesms/mms/GifSlide.java index c328d7e632..f195b37761 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/GifSlide.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/GifSlide.java @@ -14,7 +14,7 @@ public class GifSlide extends ImageSlide { public GifSlide(Attachment attachment) { super(attachment); - this.borderless = attachment.isBorderless(); + this.borderless = attachment.borderless; } public GifSlide(Context context, Uri uri, long size, int width, int height) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/ImageSlide.java b/app/src/main/java/org/thoughtcrime/securesms/mms/ImageSlide.java index f4272f4b20..0d690d4cad 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/ImageSlide.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/ImageSlide.java @@ -40,7 +40,7 @@ public class ImageSlide extends Slide { public ImageSlide(@NonNull Attachment attachment) { super(attachment); - this.borderless = attachment.isBorderless(); + this.borderless = attachment.borderless; } public ImageSlide(Context context, Uri uri, long size, int width, int height, @Nullable BlurHash blurHash) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/MediaConstraints.java b/app/src/main/java/org/thoughtcrime/securesms/mms/MediaConstraints.java index a39e0df4e0..ece54111e5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/MediaConstraints.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/MediaConstraints.java @@ -75,7 +75,7 @@ public abstract class MediaConstraints { public boolean isSatisfied(@NonNull Context context, @NonNull Attachment attachment) { try { - long size = attachment.getSize(); + long size = attachment.size; if (size > getMaxAttachmentSize()) { return false; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/PartAuthority.java b/app/src/main/java/org/thoughtcrime/securesms/mms/PartAuthority.java index a1461b58e5..96a8c8fbf4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/PartAuthority.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/PartAuthority.java @@ -97,7 +97,7 @@ public class PartAuthority { case PART_ROW: Attachment attachment = SignalDatabase.attachments().getAttachment(new PartUriParser(uri).getPartId()); - if (attachment != null) return attachment.getFileName(); + if (attachment != null) return attachment.fileName; else return null; case PERSISTENT_ROW: return DeprecatedPersistentBlobProvider.getFileName(context, uri); @@ -115,7 +115,7 @@ public class PartAuthority { case PART_ROW: Attachment attachment = SignalDatabase.attachments().getAttachment(new PartUriParser(uri).getPartId()); - if (attachment != null) return attachment.getSize(); + if (attachment != null) return attachment.size; else return null; case PERSISTENT_ROW: return DeprecatedPersistentBlobProvider.getFileSize(context, uri); @@ -133,7 +133,7 @@ public class PartAuthority { case PART_ROW: Attachment attachment = SignalDatabase.attachments().getAttachment(new PartUriParser(uri).getPartId()); - if (attachment != null) return attachment.getContentType(); + if (attachment != null) return attachment.contentType; else return null; case PERSISTENT_ROW: return DeprecatedPersistentBlobProvider.getMimeType(context, uri); @@ -151,7 +151,7 @@ public class PartAuthority { case PART_ROW: Attachment attachment = SignalDatabase.attachments().getAttachment(new PartUriParser(uri).getPartId()); - if (attachment != null) return attachment.isVideoGif(); + if (attachment != null) return attachment.videoGif; else return false; default: return false; diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/Slide.java b/app/src/main/java/org/thoughtcrime/securesms/mms/Slide.java index febdc66704..771b0ebcea 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/Slide.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/Slide.java @@ -46,7 +46,7 @@ public abstract class Slide { } public String getContentType() { - return attachment.getContentType(); + return attachment.contentType; } @Nullable @@ -69,21 +69,21 @@ public abstract class Slide { @NonNull public Optional getCaption() { - return Optional.ofNullable(attachment.getCaption()); + return Optional.ofNullable(attachment.caption); } @NonNull public Optional getFileName() { - return Optional.ofNullable(attachment.getFileName()); + return Optional.ofNullable(attachment.fileName); } @Nullable public String getFastPreflightId() { - return attachment.getFastPreflightId(); + return attachment.fastPreflightId; } public long getFileSize() { - return attachment.getSize(); + return attachment.size; } public boolean hasImage() { @@ -117,7 +117,7 @@ public abstract class Slide { } public boolean isVideoGif() { - return hasVideo() && attachment.isVideoGif(); + return hasVideo() && attachment.videoGif; } public @NonNull String getContentDescription(@NonNull Context context) { return ""; } @@ -136,7 +136,7 @@ public abstract class Slide { } public int getTransferState() { - return attachment.getTransferState(); + return attachment.transferState; } public @DrawableRes int getPlaceholderRes(Theme theme) { @@ -144,7 +144,7 @@ public abstract class Slide { } public @Nullable BlurHash getPlaceholderBlur() { - return attachment.getBlurHash(); + return attachment.blurHash; } public boolean hasPlaceholder() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/StickerSlide.java b/app/src/main/java/org/thoughtcrime/securesms/mms/StickerSlide.java index 7efe11411d..4dce46d291 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/StickerSlide.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/StickerSlide.java @@ -23,12 +23,12 @@ public class StickerSlide extends Slide { public StickerSlide(@NonNull Attachment attachment) { super(attachment); - this.stickerLocator = Objects.requireNonNull(attachment.getSticker()); + this.stickerLocator = Objects.requireNonNull(attachment.stickerLocator); } public StickerSlide(Context context, Uri uri, long size, @NonNull StickerLocator stickerLocator, @NonNull String contentType) { super(constructAttachmentFromUri(context, uri, contentType, size, WIDTH, HEIGHT, true, null, null, stickerLocator, null, null, false, false, false, false)); - this.stickerLocator = Objects.requireNonNull(attachment.getSticker()); + this.stickerLocator = Objects.requireNonNull(attachment.stickerLocator); } @Override @@ -52,6 +52,6 @@ public class StickerSlide extends Slide { } public @Nullable String getEmoji() { - return stickerLocator.getEmoji(); + return stickerLocator.emoji; } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/providers/PartProvider.java b/app/src/main/java/org/thoughtcrime/securesms/providers/PartProvider.java index f1d701135f..e8f8ccc464 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/providers/PartProvider.java +++ b/app/src/main/java/org/thoughtcrime/securesms/providers/PartProvider.java @@ -134,8 +134,8 @@ public final class PartProvider extends BaseContentProvider { DatabaseAttachment attachment = SignalDatabase.attachments().getAttachment(partUriParser.getPartId()); if (attachment != null) { - Log.i(TAG, "getType() called: " + uri + " It's " + attachment.getContentType()); - return attachment.getContentType(); + Log.i(TAG, "getType() called: " + uri + " It's " + attachment.contentType); + return attachment.contentType; } } @@ -163,15 +163,15 @@ public final class PartProvider extends BaseContentProvider { if (attachment == null) return null; - long fileSize = attachment.getSize(); + long fileSize = attachment.size; if (fileSize <= 0) { Log.w(TAG, "Empty file " + fileSize); return null; } - String fileName = attachment.getFileName() != null ? attachment.getFileName() - : createFileNameForMimeType(attachment.getContentType()); + String fileName = attachment.fileName != null ? attachment.fileName + : createFileNameForMimeType(attachment.contentType); return createCursor(projection, fileName, fileSize); } else { @@ -229,9 +229,9 @@ public final class PartProvider extends BaseContentProvider { @Override public long onGetSize() throws ErrnoException { DatabaseAttachment attachment = attachments.getAttachment(attachmentId); - if (attachment != null && attachment.getSize() > 0) { + if (attachment != null && attachment.size > 0) { Log.i(TAG, attachmentId + ":getSize"); - return attachment.getSize(); + return attachment.size; } else { Log.w(TAG, attachmentId + ":getSize:attachment is null or size is 0"); throw new ErrnoException("Attachment is invalid", OsConstants.ENOENT); @@ -242,7 +242,7 @@ public final class PartProvider extends BaseContentProvider { public int onRead(long offset, int size, byte[] data) throws ErrnoException { try { DatabaseAttachment attachment = attachments.getAttachment(attachmentId); - if (attachment == null || attachment.getSize() <= 0) { + if (attachment == null || attachment.size <= 0) { Log.w(TAG, attachmentId + ":onRead:attachment is null or size is 0"); throw new ErrnoException("Attachment is invalid", OsConstants.ENOENT); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/revealable/ViewOnceMessageView.java b/app/src/main/java/org/thoughtcrime/securesms/revealable/ViewOnceMessageView.java index 6b46bdc9af..b7dd4fece1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/revealable/ViewOnceMessageView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/revealable/ViewOnceMessageView.java @@ -95,8 +95,8 @@ public class ViewOnceMessageView extends LinearLayout { } Attachment attachment = messageRecord.getSlideDeck().getThumbnailSlide().asAttachment(); - return attachment.getTransferState() == AttachmentTable.TRANSFER_PROGRESS_FAILED || - attachment.getTransferState() == AttachmentTable.TRANSFER_PROGRESS_PENDING; + return attachment.transferState == AttachmentTable.TRANSFER_PROGRESS_FAILED || + attachment.transferState == AttachmentTable.TRANSFER_PROGRESS_PENDING; } public void setMessage(@NonNull MmsMessageRecord message, boolean hasWallpaper) { @@ -169,7 +169,7 @@ public class ViewOnceMessageView extends LinearLayout { if (messageRecord.getSlideDeck().getThumbnailSlide() == null) return false; Attachment attachment = messageRecord.getSlideDeck().getThumbnailSlide().asAttachment(); - return attachment.getTransferState() == AttachmentTable.TRANSFER_PROGRESS_STARTED; + return attachment.transferState == AttachmentTable.TRANSFER_PROGRESS_STARTED; } private @NonNull String formatFileSize(@NonNull MmsMessageRecord messageRecord) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/sharing/MultiShareSender.java b/app/src/main/java/org/thoughtcrime/securesms/sharing/MultiShareSender.java index 0ec58a4376..355e5103ca 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/sharing/MultiShareSender.java +++ b/app/src/main/java/org/thoughtcrime/securesms/sharing/MultiShareSender.java @@ -329,15 +329,15 @@ public final class MultiShareSender { : thumbnail.getUri() == null ? null : new ImageSlide(context, - thumbnail.getUri(), - thumbnail.getContentType(), - thumbnail.getSize(), - thumbnail.getWidth(), - thumbnail.getHeight(), - thumbnail.isBorderless(), - thumbnail.getCaption(), - thumbnail.getBlurHash(), - thumbnail.getTransformProperties()).asAttachment() + thumbnail.getUri(), + thumbnail.contentType, + thumbnail.size, + thumbnail.width, + thumbnail.height, + thumbnail.borderless, + thumbnail.caption, + thumbnail.blurHash, + thumbnail.transformProperties).asAttachment() ) )); } @@ -345,17 +345,17 @@ public final class MultiShareSender { private static Slide ensureDefaultQuality(@NonNull Context context, @NonNull ImageSlide imageSlide) { Attachment attachment = imageSlide.asAttachment(); - if (attachment.getTransformProperties().getSentMediaQuality() == SentMediaQuality.HIGH.getCode()) { + if (attachment.transformProperties.sentMediaQuality == SentMediaQuality.HIGH.getCode()) { return new ImageSlide( context, attachment.getUri(), - attachment.getContentType(), - attachment.getSize(), - attachment.getWidth(), - attachment.getHeight(), - attachment.isBorderless(), - attachment.getCaption(), - attachment.getBlurHash(), + attachment.contentType, + attachment.size, + attachment.width, + attachment.height, + attachment.borderless, + attachment.caption, + attachment.blurHash, AttachmentTable.TransformProperties.empty() ); } else { diff --git a/app/src/main/java/org/thoughtcrime/securesms/sms/MessageSender.java b/app/src/main/java/org/thoughtcrime/securesms/sms/MessageSender.java index 55b52d5d6f..47e03bc6b4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/sms/MessageSender.java +++ b/app/src/main/java/org/thoughtcrime/securesms/sms/MessageSender.java @@ -344,7 +344,7 @@ public class MessageSender { List attachmentIds = new ArrayList<>(preUploadAttachmentIds.size()); for (int i = 0; i < preUploadAttachments.size(); i++) { - AttachmentId attachmentId = attachmentDatabase.insertAttachmentForPreUpload(preUploadAttachments.get(i)).getAttachmentId(); + AttachmentId attachmentId = attachmentDatabase.insertAttachmentForPreUpload(preUploadAttachments.get(i)).attachmentId; attachmentCopies.get(i).add(attachmentId); attachmentIds.add(attachmentId); } @@ -418,14 +418,14 @@ public class MessageSender { DatabaseAttachment databaseAttachment = attachmentDatabase.insertAttachmentForPreUpload(attachment); Job compressionJob = AttachmentCompressionJob.fromAttachment(databaseAttachment, false, -1); - Job uploadJob = new AttachmentUploadJob(databaseAttachment.getAttachmentId()); + Job uploadJob = new AttachmentUploadJob(databaseAttachment.attachmentId); ApplicationDependencies.getJobManager() .startChain(compressionJob) .then(uploadJob) .enqueue(); - return new PreUploadResult(media, databaseAttachment.getAttachmentId(), Arrays.asList(compressionJob.getId(), uploadJob.getId())); + return new PreUploadResult(media, databaseAttachment.attachmentId, Arrays.asList(compressionJob.getId(), uploadJob.getId())); } catch (MmsException e) { Log.w(TAG, "preUploadPushAttachment() - Failed to upload!", e); return null; @@ -645,7 +645,7 @@ public class MessageSender { .toList(); List fakeUploadJobs = Stream.of(attachments) - .map(a -> new AttachmentMarkUploadedJob(messageId, ((DatabaseAttachment) a).getAttachmentId())) + .map(a -> new AttachmentMarkUploadedJob(messageId, ((DatabaseAttachment) a).attachmentId)) .toList(); ApplicationDependencies.getJobManager().startChain(compressionJobs) diff --git a/app/src/main/java/org/thoughtcrime/securesms/sms/UploadDependencyGraph.kt b/app/src/main/java/org/thoughtcrime/securesms/sms/UploadDependencyGraph.kt index b2d475e0e7..1786a86f5e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/sms/UploadDependencyGraph.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/sms/UploadDependencyGraph.kt @@ -6,6 +6,7 @@ import org.thoughtcrime.securesms.attachments.AttachmentId import org.thoughtcrime.securesms.attachments.DatabaseAttachment import org.thoughtcrime.securesms.attachments.UriAttachment import org.thoughtcrime.securesms.database.AttachmentTable +import org.thoughtcrime.securesms.database.AttachmentTable.TransformProperties import org.thoughtcrime.securesms.jobmanager.Job import org.thoughtcrime.securesms.jobmanager.JobManager import org.thoughtcrime.securesms.jobs.AttachmentCompressionJob @@ -43,7 +44,7 @@ class UploadDependencyGraph private constructor( */ private data class AttachmentKey( val attachment: A, - private val transformProperties: AttachmentTable.TransformProperties = attachment.transformProperties + private val transformProperties: AttachmentTable.TransformProperties = attachment.transformProperties ?: AttachmentTable.TransformProperties.empty() ) private var hasConsumedJobQueue = false @@ -75,14 +76,14 @@ class UploadDependencyGraph private constructor( * Allows representation of a unique database attachment by its internal id and its transform properties. */ private fun DatabaseAttachment.asDatabaseAttachmentKey(): AttachmentKey { - return AttachmentKey(this, this.transformProperties) + return AttachmentKey(this, this.transformProperties ?: TransformProperties.empty()) } /** * Allows representation of a unique URI attachment by its internal Uri and its transform properties. */ private fun UriAttachment.asUriAttachmentKey(): AttachmentKey { - return AttachmentKey(this, transformProperties) + return AttachmentKey(this, transformProperties ?: TransformProperties.empty()) } /** @@ -119,7 +120,7 @@ class UploadDependencyGraph private constructor( message.linkPreviews.mapNotNull { it.thumbnail.orElse(null) } + message.sharedContacts.mapNotNull { it.avatar?.attachment } - val uniqueAttachments: Set> = attachmentList.map { AttachmentKey(it, it.transformProperties) }.toSet() + val uniqueAttachments: Set> = attachmentList.map { AttachmentKey(it, it.transformProperties ?: TransformProperties.empty()) }.toSet() for (attachmentKey in uniqueAttachments) { when (val attachment = attachmentKey.attachment) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerLocator.java b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerLocator.java deleted file mode 100644 index ccf474571c..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerLocator.java +++ /dev/null @@ -1,70 +0,0 @@ -package org.thoughtcrime.securesms.stickers; - -import android.os.Parcel; -import android.os.Parcelable; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -public class StickerLocator implements Parcelable { - - private final String packId; - private final String packKey; - private final int stickerId; - private final String emoji; - - public StickerLocator(@NonNull String packId, @NonNull String packKey, int stickerId, @Nullable String emoji) { - this.packId = packId; - this.packKey = packKey; - this.stickerId = stickerId; - this.emoji = emoji; - } - - private StickerLocator(Parcel in) { - packId = in.readString(); - packKey = in.readString(); - stickerId = in.readInt(); - emoji = in.readString(); - } - - public @NonNull String getPackId() { - return packId; - } - - public @NonNull String getPackKey() { - return packKey; - } - - public int getStickerId() { - return stickerId; - } - - public @Nullable String getEmoji() { - return emoji; - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeString(packId); - dest.writeString(packKey); - dest.writeInt(stickerId); - dest.writeString(emoji); - } - - public static final Creator CREATOR = new Creator() { - @Override - public StickerLocator createFromParcel(Parcel in) { - return new StickerLocator(in); - } - - @Override - public StickerLocator[] newArray(int size) { - return new StickerLocator[size]; - } - }; -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerLocator.kt b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerLocator.kt new file mode 100644 index 0000000000..47749a43c5 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerLocator.kt @@ -0,0 +1,16 @@ +package org.thoughtcrime.securesms.stickers + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +class StickerLocator( + @JvmField + val packId: String, + @JvmField + val packKey: String, + @JvmField + val stickerId: Int, + @JvmField + val emoji: String? +) : Parcelable diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/Stories.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/Stories.kt index c5bcc26e93..3e48cae81a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/Stories.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/Stories.kt @@ -246,7 +246,7 @@ object Stories { return if (MediaUtil.isVideo(media.mimeType)) { val mediaDuration = if (media.duration == 0L && media.transformProperties.map(TransformProperties::shouldSkipTransform).orElse(true)) { getVideoDuration(media.uri) - } else if (media.transformProperties.map { it.isVideoTrim }.orElse(false)) { + } else if (media.transformProperties.map { it.videoTrim }.orElse(false)) { TimeUnit.MICROSECONDS.toMillis(media.transformProperties.get().videoTrimEndTimeUs - media.transformProperties.get().videoTrimStartTimeUs) } else { media.duration diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/post/StoryPostViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/post/StoryPostViewModel.kt index da2887ba5a..a1389f3ace 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/post/StoryPostViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/post/StoryPostViewModel.kt @@ -41,18 +41,18 @@ class StoryPostViewModel(private val repository: StoryTextPostRepository) : View store.update { StoryPostState.None() } } else if (storyPostContent.isVideo()) { store.update { - val shouldSkipTransform = storyPostContent.attachment.transformProperties.shouldSkipTransform() + val shouldSkipTransform = storyPostContent.attachment.transformProperties?.shouldSkipTransform() == true val clipStart: Duration = if (shouldSkipTransform) { 0L.microseconds } else { - storyPostContent.attachment.transformProperties.videoTrimStartTimeUs.microseconds + storyPostContent.attachment.transformProperties?.videoTrimStartTimeUs?.microseconds ?: 0L.microseconds } val clipEnd: Duration = if (shouldSkipTransform) { 0L.microseconds } else { - storyPostContent.attachment.transformProperties.videoTrimEndTimeUs.microseconds + storyPostContent.attachment.transformProperties?.videoTrimEndTimeUs?.microseconds ?: 0L.microseconds } StoryPostState.VideoPost( diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/AttachmentUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/AttachmentUtil.java index 5a5999ecb9..564594dee6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/AttachmentUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/AttachmentUtil.java @@ -37,15 +37,15 @@ public class AttachmentUtil { } Set allowedTypes = getAllowedAutoDownloadTypes(context); - String contentType = attachment.getContentType(); + String contentType = attachment.contentType; - if (attachment.isVoiceNote() || - (MediaUtil.isAudio(attachment) && TextUtils.isEmpty(attachment.getFileName())) || - MediaUtil.isLongTextType(attachment.getContentType()) || + if (attachment.voiceNote || + (MediaUtil.isAudio(attachment) && TextUtils.isEmpty(attachment.fileName)) || + MediaUtil.isLongTextType(attachment.contentType) || attachment.isSticker()) { return true; - } else if (attachment.isVideoGif()) { + } else if (attachment.videoGif) { boolean allowed = NotInCallConstraint.isNotInConnectedCall() && allowedTypes.contains("image"); if (!allowed) { Log.w(TAG, "Not auto downloading. inCall: " + NotInCallConstraint.isNotInConnectedCall() + " allowedType: " + allowedTypes.contains("image")); @@ -74,8 +74,8 @@ public class AttachmentUtil { public static void deleteAttachment(@NonNull Context context, @NonNull DatabaseAttachment attachment) { - AttachmentId attachmentId = attachment.getAttachmentId(); - long mmsId = attachment.getMmsId(); + AttachmentId attachmentId = attachment.attachmentId; + long mmsId = attachment.mmsId; int attachmentCount = SignalDatabase.attachments() .getAttachmentsForMessage(mmsId) .size(); @@ -104,7 +104,7 @@ public class AttachmentUtil { @WorkerThread private static boolean isFromTrustedConversation(@NonNull Context context, @NonNull DatabaseAttachment attachment) { try { - MessageRecord message = SignalDatabase.messages().getMessageRecord(attachment.getMmsId()); + MessageRecord message = SignalDatabase.messages().getMessageRecord(attachment.mmsId); Recipient fromRecipient = message.getFromRecipient(); Recipient toRecipient = SignalDatabase.threads().getRecipientForThreadId(message.getThreadId()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/MediaUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/MediaUtil.java index 80af665609..d59e3b0621 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/MediaUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/MediaUtil.java @@ -100,7 +100,7 @@ public class MediaUtil { return new StickerSlide(attachment); } - switch (getSlideTypeFromContentType(attachment.getContentType())) { + switch (getSlideTypeFromContentType(attachment.contentType)) { case GIF : return new GifSlide(attachment); case IMAGE : return new ImageSlide(attachment); case VIDEO : return new VideoSlide(attachment); @@ -262,31 +262,31 @@ public class MediaUtil { } public static boolean isGif(Attachment attachment) { - return isGif(attachment.getContentType()); + return isGif(attachment.contentType); } public static boolean isJpeg(Attachment attachment) { - return isJpegType(attachment.getContentType()); + return isJpegType(attachment.contentType); } public static boolean isHeic(Attachment attachment) { - return isHeicType(attachment.getContentType()); + return isHeicType(attachment.contentType); } public static boolean isHeif(Attachment attachment) { - return isHeifType(attachment.getContentType()); + return isHeifType(attachment.contentType); } public static boolean isImage(Attachment attachment) { - return isImageType(attachment.getContentType()); + return isImageType(attachment.contentType); } public static boolean isAudio(Attachment attachment) { - return isAudioType(attachment.getContentType()); + return isAudioType(attachment.contentType); } public static boolean isVideo(Attachment attachment) { - return isVideoType(attachment.getContentType()); + return isVideoType(attachment.contentType); } public static boolean isVideo(String contentType) { @@ -473,7 +473,7 @@ public class MediaUtil { } final Attachment attachment = slide.asAttachment(); final boolean isIncremental = attachment.getIncrementalDigest() != null; - final boolean hasIncrementalMacChunkSizeDefined = attachment.getIncrementalMacChunkSize() > 0; + final boolean hasIncrementalMacChunkSizeDefined = attachment.incrementalMacChunkSize > 0; final boolean contentTypeSupported = isVideoType(slide.getContentType()); return isIncremental && contentTypeSupported && hasIncrementalMacChunkSizeDefined; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/video/exo/PartDataSource.java b/app/src/main/java/org/thoughtcrime/securesms/video/exo/PartDataSource.java index 2fe498c6db..7d50d14aa2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/video/exo/PartDataSource.java +++ b/app/src/main/java/org/thoughtcrime/securesms/video/exo/PartDataSource.java @@ -58,13 +58,14 @@ class PartDataSource implements DataSource { final boolean hasIncrementalDigest = attachment.getIncrementalDigest() != null; final boolean inProgress = attachment.isInProgress(); - final String attachmentKey = attachment.getKey(); - final boolean hasData = attachment.hasData(); + final String attachmentKey = attachment.key; + final boolean hasData = attachment.hasData; + if (inProgress && !hasData && hasIncrementalDigest && attachmentKey != null && FeatureFlags.instantVideoPlayback()) { final byte[] decode = Base64.decode(attachmentKey); - final File transferFile = attachmentDatabase.getOrCreateTransferFile(attachment.getAttachmentId()); + final File transferFile = attachmentDatabase.getOrCreateTransferFile(attachment.attachmentId); try { - this.inputStream = AttachmentCipherInputStream.createForAttachment(transferFile, attachment.getSize(), decode, attachment.getDigest(), attachment.getIncrementalDigest(), attachment.getIncrementalMacChunkSize()); + this.inputStream = AttachmentCipherInputStream.createForAttachment(transferFile, attachment.size, decode, attachment.digest, attachment.getIncrementalDigest(), attachment.incrementalMacChunkSize); long skipped = 0; while (skipped < dataSpec.position) { @@ -81,8 +82,8 @@ class PartDataSource implements DataSource { Log.d(TAG, "Successfully loaded completed attachment file."); } else { - throw new IOException("Ineligible " + attachment.getAttachmentId().toString() - + "\nTransfer state: " + attachment.getTransferState() + throw new IOException("Ineligible " + attachment.attachmentId.toString() + + "\nTransfer state: " + attachment.transferState + "\nIncremental Digest Present: " + hasIncrementalDigest + "\nAttachment Key Non-Empty: " + (attachmentKey != null && !attachmentKey.isEmpty())); } @@ -91,9 +92,9 @@ class PartDataSource implements DataSource { listener.onTransferStart(this, dataSpec, false); } - if (attachment.getSize() - dataSpec.position <= 0) throw new EOFException("No more data"); + if (attachment.size - dataSpec.position <= 0) throw new EOFException("No more data"); - return attachment.getSize() - dataSpec.position; + return attachment.size - dataSpec.position; } @Override diff --git a/app/src/test/java/org/thoughtcrime/securesms/database/AttachmentDatabaseTransformPropertiesTest.java b/app/src/test/java/org/thoughtcrime/securesms/database/AttachmentDatabaseTransformPropertiesTest.java index 7a32bcd172..dc52b93568 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/database/AttachmentDatabaseTransformPropertiesTest.java +++ b/app/src/test/java/org/thoughtcrime/securesms/database/AttachmentDatabaseTransformPropertiesTest.java @@ -21,8 +21,8 @@ public class AttachmentDatabaseTransformPropertiesTest { AttachmentTable.TransformProperties properties = AttachmentTable.TransformProperties.parse(json); - assertEquals(0, properties.getSentMediaQuality()); - assertEquals(SentMediaQuality.STANDARD, SentMediaQuality.fromCode(properties.getSentMediaQuality())); + assertEquals(0, properties.sentMediaQuality); + assertEquals(SentMediaQuality.STANDARD, SentMediaQuality.fromCode(properties.sentMediaQuality)); } } \ No newline at end of file diff --git a/app/src/test/java/org/thoughtcrime/securesms/sms/UploadDependencyGraphTest.kt b/app/src/test/java/org/thoughtcrime/securesms/sms/UploadDependencyGraphTest.kt index 48e85886b3..0528f11d8a 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/sms/UploadDependencyGraphTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/sms/UploadDependencyGraphTest.kt @@ -228,35 +228,35 @@ class UploadDependencyGraphTest { private fun getAttachmentForPreUpload(id: Long, attachment: Attachment): DatabaseAttachment { return DatabaseAttachment( - AttachmentId(id, id), - AttachmentTable.PREUPLOAD_MESSAGE_ID, - false, - false, - attachment.contentType, - AttachmentTable.TRANSFER_PROGRESS_PENDING, - attachment.size, - attachment.fileName, - attachment.cdnNumber, - attachment.location, - attachment.key, - attachment.relay, - attachment.digest, - attachment.incrementalDigest, - attachment.incrementalMacChunkSize, - attachment.fastPreflightId, - attachment.isVoiceNote, - attachment.isBorderless, - attachment.isVideoGif, - attachment.width, - attachment.height, - attachment.isQuote, - attachment.caption, - attachment.sticker, - attachment.blurHash, - attachment.audioHash, - attachment.transformProperties, - 0, - attachment.uploadTimestamp + attachmentId = AttachmentId(id, id), + mmsId = AttachmentTable.PREUPLOAD_MESSAGE_ID, + hasData = false, + hasThumbnail = false, + contentType = attachment.contentType, + transferProgress = AttachmentTable.TRANSFER_PROGRESS_PENDING, + size = attachment.size, + fileName = attachment.fileName, + cdnNumber = attachment.cdnNumber, + location = attachment.location, + key = attachment.key, + relay = attachment.relay, + digest = attachment.digest, + incrementalDigest = attachment.incrementalDigest, + incrementalMacChunkSize = attachment.incrementalMacChunkSize, + fastPreflightId = attachment.fastPreflightId, + voiceNote = attachment.voiceNote, + borderless = attachment.borderless, + videoGif = attachment.videoGif, + width = attachment.width, + height = attachment.height, + quote = attachment.quote, + caption = attachment.caption, + stickerLocator = attachment.stickerLocator, + blurHash = attachment.blurHash, + audioHash = attachment.audioHash, + transformProperties = attachment.transformProperties, + displayOrder = 0, + uploadTimestamp = attachment.uploadTimestamp ) } diff --git a/core-util/src/main/java/org/signal/core/util/CursorExtensions.kt b/core-util/src/main/java/org/signal/core/util/CursorExtensions.kt index 80a4cd72bc..dd8d710567 100644 --- a/core-util/src/main/java/org/signal/core/util/CursorExtensions.kt +++ b/core-util/src/main/java/org/signal/core/util/CursorExtensions.kt @@ -148,6 +148,23 @@ inline fun Cursor.readToMap(predicate: (Pair) -> Boolean = { true } return readToList(predicate, mapper).associate { it } } +/** + * Groups the cursor by the given key, and returns a map of keys to lists of values. + */ +inline fun Cursor.groupBy(mapper: (Cursor) -> Pair): Map> { + val map: MutableMap> = mutableMapOf() + + use { + while (moveToNext()) { + val pair = mapper(this) + val list = map.getOrPut(pair.first) { mutableListOf() } + list += pair.second + } + } + + return map +} + inline fun Cursor.readToSet(predicate: (T) -> Boolean = { true }, mapper: (Cursor) -> T): Set { val set = mutableSetOf() use {