mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-05-30 12:03:08 +01:00
Adjust auto-download checks.
This commit is contained in:
committed by
Greyson Parrelli
parent
fdcd21132c
commit
561186df90
+16
@@ -27,6 +27,7 @@ import org.signal.core.util.bytes
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.compose.rememberStatusBarColorNestedScrollModifier
|
||||
import org.thoughtcrime.securesms.mms.SentMediaQuality
|
||||
import org.thoughtcrime.securesms.util.AttachmentUtil
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
import org.thoughtcrime.securesms.webrtc.CallDataMode
|
||||
import kotlin.math.abs
|
||||
@@ -169,6 +170,21 @@ private fun DataAndStorageSettingsScreen(
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Rows.TextRow(
|
||||
text = {
|
||||
Text(
|
||||
text = stringResource(
|
||||
R.string.DataAndStorageSettingsFragment__voice_messages_and_stickers_under_size_are_always_auto_downloaded,
|
||||
AttachmentUtil.SMALL_ATTACHMENT_SIZE.toUnitString()
|
||||
),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Dividers.Default()
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ import org.thoughtcrime.securesms.transport.RetryLaterException
|
||||
import org.thoughtcrime.securesms.util.AttachmentUtil
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||
import org.whispersystems.signalservice.api.crypto.AttachmentCipherInputStream.IntegrityCheck
|
||||
import org.whispersystems.signalservice.api.crypto.AttachmentCipherStreamUtil
|
||||
import org.whispersystems.signalservice.api.messages.AttachmentTransferProgress
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer
|
||||
@@ -46,6 +47,7 @@ import org.whispersystems.signalservice.api.push.exceptions.MissingConfiguration
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException
|
||||
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException
|
||||
import org.whispersystems.signalservice.api.push.exceptions.RangeException
|
||||
import org.whispersystems.signalservice.internal.crypto.PaddingInputStream
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.util.Optional
|
||||
@@ -140,14 +142,16 @@ class AttachmentDownloadJob private constructor(
|
||||
}
|
||||
}
|
||||
|
||||
constructor(messageId: Long, attachmentId: AttachmentId, forceDownload: Boolean) : this(
|
||||
constructor(messageId: Long, attachmentId: AttachmentId, forceDownload: Boolean) : this(messageId, attachmentId, forceDownload, forceDownload, forceDownload)
|
||||
|
||||
constructor(messageId: Long, attachmentId: AttachmentId, forceDownload: Boolean, skipInCallConstraint: Boolean, isHighPriority: Boolean) : this(
|
||||
Parameters.Builder()
|
||||
.setQueue(constructQueueString(attachmentId))
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.maybeApplyNotInCallConstraint(forceDownload)
|
||||
.maybeApplyNotInCallConstraint(skipInCallConstraint)
|
||||
.setLifespan(TimeUnit.DAYS.toMillis(1))
|
||||
.setMaxAttempts(Parameters.UNLIMITED)
|
||||
.setQueuePriority(if (forceDownload) Parameters.PRIORITY_HIGH else Parameters.PRIORITY_DEFAULT)
|
||||
.setQueuePriority(if (isHighPriority) Parameters.PRIORITY_HIGH else Parameters.PRIORITY_DEFAULT)
|
||||
.build(),
|
||||
messageId,
|
||||
attachmentId,
|
||||
@@ -314,12 +318,20 @@ class AttachmentDownloadJob private constructor(
|
||||
throw InvalidAttachmentException("Attachment has no integrity check!")
|
||||
}
|
||||
|
||||
if (attachment.size <= 0) {
|
||||
Log.w(TAG, "[$attachmentId] Attachment has no declared size!")
|
||||
throw InvalidAttachmentException("Attachment has no declared size!")
|
||||
}
|
||||
|
||||
val expectedCiphertextSize = AttachmentCipherStreamUtil.getCiphertextLength(PaddingInputStream.getPaddedSize(attachment.size))
|
||||
val downloadLimit: Long = minOf(expectedCiphertextSize, maxReceiveSize)
|
||||
|
||||
val decryptingStream = AppDependencies
|
||||
.signalServiceMessageReceiver
|
||||
.retrieveAttachment(
|
||||
pointer,
|
||||
attachmentFile,
|
||||
maxReceiveSize,
|
||||
downloadLimit,
|
||||
IntegrityCheck.forEncryptedDigestAndPlaintextHash(attachment.remoteDigest, attachment.dataHash),
|
||||
progressListener
|
||||
)
|
||||
@@ -470,8 +482,8 @@ class AttachmentDownloadJob private constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun Parameters.Builder.maybeApplyNotInCallConstraint(forceDownload: Boolean): Parameters.Builder {
|
||||
if (forceDownload) {
|
||||
private fun Parameters.Builder.maybeApplyNotInCallConstraint(skipConstraint: Boolean): Parameters.Builder {
|
||||
if (skipConstraint) {
|
||||
return this
|
||||
}
|
||||
return this.addConstraint(NotInCallConstraint.KEY)
|
||||
|
||||
@@ -973,14 +973,16 @@ object DataMessageProcessor {
|
||||
SignalDatabase.runPostSuccessfulTransaction {
|
||||
if (insertResult.insertedAttachments != null) {
|
||||
val downloadJobs: List<AttachmentDownloadJob> = insertResult.insertedAttachments.mapNotNull { (attachment, attachmentId) ->
|
||||
if (attachment.isSticker) {
|
||||
if (attachment.transferState != AttachmentTable.TRANSFER_PROGRESS_DONE) {
|
||||
AttachmentDownloadJob(messageId = insertResult.messageId, attachmentId = attachmentId, forceDownload = true)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
if (attachment.transferState != AttachmentTable.TRANSFER_PROGRESS_DONE) {
|
||||
AttachmentDownloadJob(
|
||||
messageId = insertResult.messageId,
|
||||
attachmentId = attachmentId,
|
||||
forceDownload = false,
|
||||
skipInCallConstraint = attachment.isSticker,
|
||||
isHighPriority = attachment.isSticker
|
||||
)
|
||||
} else {
|
||||
AttachmentDownloadJob(messageId = insertResult.messageId, attachmentId = attachmentId, forceDownload = false)
|
||||
null
|
||||
}
|
||||
}
|
||||
AppDependencies.jobManager.addAll(downloadJobs)
|
||||
|
||||
@@ -1,153 +0,0 @@
|
||||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.MainThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
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.database.NoSuchMessageException;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NotInCallConstraint;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceDeleteSyncJob;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
public class AttachmentUtil {
|
||||
|
||||
private static final String TAG = Log.tag(AttachmentUtil.class);
|
||||
|
||||
@MainThread
|
||||
public static boolean isRestoreOnOpenPermitted(@NonNull Context context, @Nullable Attachment attachment) {
|
||||
if (attachment == null) {
|
||||
Log.w(TAG, "attachment was null, returning vacuous true");
|
||||
return true;
|
||||
}
|
||||
Set<String> allowedTypes = getAllowedAutoDownloadTypes(context);
|
||||
String contentType = attachment.contentType;
|
||||
|
||||
if (MediaUtil.isImageType(contentType)) {
|
||||
return NotInCallConstraint.isNotInConnectedCall() && allowedTypes.contains(MediaUtil.getDiscreteMimeType(contentType));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public static boolean isAutoDownloadPermitted(@NonNull Context context, @Nullable DatabaseAttachment attachment) {
|
||||
if (attachment == null) {
|
||||
Log.w(TAG, "attachment was null, returning vacuous true");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!isFromTrustedConversation(context, attachment)) {
|
||||
Log.w(TAG, "Not allowing download due to untrusted conversation");
|
||||
return false;
|
||||
}
|
||||
|
||||
Set<String> allowedTypes = getAllowedAutoDownloadTypes(context);
|
||||
String contentType = attachment.contentType;
|
||||
|
||||
if (attachment.voiceNote ||
|
||||
(MediaUtil.isAudio(attachment) && TextUtils.isEmpty(attachment.fileName)) ||
|
||||
MediaUtil.isLongTextType(attachment.contentType) ||
|
||||
attachment.isSticker())
|
||||
{
|
||||
return true;
|
||||
} 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"));
|
||||
}
|
||||
return allowed;
|
||||
} else if (isNonDocumentType(contentType)) {
|
||||
boolean allowed = NotInCallConstraint.isNotInConnectedCall() && allowedTypes.contains(MediaUtil.getDiscreteMimeType(contentType));
|
||||
if (!allowed) {
|
||||
Log.w(TAG, "Not auto downloading. inCall: " + !NotInCallConstraint.isNotInConnectedCall() + " allowedType: " + allowedTypes.contains(MediaUtil.getDiscreteMimeType(contentType)));
|
||||
}
|
||||
return allowed;
|
||||
} else {
|
||||
boolean allowed = NotInCallConstraint.isNotInConnectedCall() && allowedTypes.contains("documents");
|
||||
if (!allowed) {
|
||||
Log.w(TAG, "Not auto downloading. inCall: " + !NotInCallConstraint.isNotInConnectedCall() + " allowedType: " + allowedTypes.contains("documents"));
|
||||
}
|
||||
return allowed;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the specified attachment. If its the only attachment for its linked message, the entire
|
||||
* message is deleted.
|
||||
*
|
||||
* @return message record of deleted message if a message is deleted
|
||||
*/
|
||||
@WorkerThread
|
||||
public static @Nullable MessageRecord deleteAttachment(@NonNull DatabaseAttachment attachment) {
|
||||
AttachmentId attachmentId = attachment.attachmentId;
|
||||
long mmsId = attachment.mmsId;
|
||||
int attachmentCount = SignalDatabase.attachments()
|
||||
.getAttachmentsForMessage(mmsId)
|
||||
.size();
|
||||
|
||||
MessageRecord deletedMessageRecord = null;
|
||||
if (attachmentCount <= 1) {
|
||||
deletedMessageRecord = SignalDatabase.messages().getMessageRecordOrNull(mmsId);
|
||||
SignalDatabase.messages().deleteMessage(mmsId);
|
||||
} else {
|
||||
SignalDatabase.attachments().deleteAttachment(attachmentId);
|
||||
MultiDeviceDeleteSyncJob.enqueueAttachmentDelete(SignalDatabase.messages().getMessageRecordOrNull(mmsId), attachment);
|
||||
}
|
||||
|
||||
return deletedMessageRecord;
|
||||
}
|
||||
|
||||
private static boolean isNonDocumentType(String contentType) {
|
||||
return
|
||||
MediaUtil.isImageType(contentType) ||
|
||||
MediaUtil.isVideoType(contentType) ||
|
||||
MediaUtil.isAudioType(contentType);
|
||||
}
|
||||
|
||||
private static @NonNull Set<String> getAllowedAutoDownloadTypes(@NonNull Context context) {
|
||||
if (NetworkUtil.isConnectedWifi(context)) return TextSecurePreferences.getWifiMediaDownloadAllowed(context);
|
||||
else if (NetworkUtil.isConnectedRoaming(context)) return TextSecurePreferences.getRoamingMediaDownloadAllowed(context);
|
||||
else if (NetworkUtil.isConnectedMobile(context)) return TextSecurePreferences.getMobileMediaDownloadAllowed(context);
|
||||
else return Collections.emptySet();
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private static boolean isFromTrustedConversation(@NonNull Context context, @NonNull DatabaseAttachment attachment) {
|
||||
try {
|
||||
MessageRecord message = SignalDatabase.messages().getMessageRecord(attachment.mmsId);
|
||||
|
||||
Recipient fromRecipient = message.getFromRecipient();
|
||||
Recipient toRecipient = SignalDatabase.threads().getRecipientForThreadId(message.getThreadId());
|
||||
|
||||
if (toRecipient != null && toRecipient.isGroup()) {
|
||||
return toRecipient.isProfileSharing() || isTrustedIndividual(fromRecipient, message);
|
||||
} else {
|
||||
return isTrustedIndividual(fromRecipient, message);
|
||||
}
|
||||
} catch (NoSuchMessageException e) {
|
||||
Log.w(TAG, "Message could not be found! Assuming not a trusted contact.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isTrustedIndividual(@NonNull Recipient recipient, @NonNull MessageRecord message) {
|
||||
return recipient.isSystemContact() ||
|
||||
recipient.isProfileSharing() ||
|
||||
message.isOutgoing() ||
|
||||
recipient.isSelf() ||
|
||||
recipient.isReleaseNotes();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
package org.thoughtcrime.securesms.util
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.annotation.WorkerThread
|
||||
import org.signal.core.util.ByteSize
|
||||
import org.signal.core.util.bytes
|
||||
import org.signal.core.util.kibiBytes
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.mebiBytes
|
||||
import org.thoughtcrime.securesms.attachments.Attachment
|
||||
import org.thoughtcrime.securesms.attachments.Cdn
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
|
||||
import org.thoughtcrime.securesms.database.NoSuchMessageException
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.attachments
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.messages
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.threads
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NotInCallConstraint
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceDeleteSyncJob.Companion.enqueueAttachmentDelete
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.whispersystems.signalservice.api.crypto.AttachmentCipherStreamUtil
|
||||
import org.whispersystems.signalservice.internal.crypto.PaddingInputStream
|
||||
|
||||
object AttachmentUtil {
|
||||
private val TAG = Log.tag(AttachmentUtil::class.java)
|
||||
|
||||
private val MAX_AUTO_DOWNLOAD_SIZE: ByteSize = 200.mebiBytes
|
||||
val SMALL_ATTACHMENT_SIZE: ByteSize = 100.kibiBytes
|
||||
|
||||
@JvmStatic
|
||||
@MainThread
|
||||
fun isRestoreOnOpenPermitted(context: Context, attachment: Attachment?): Boolean {
|
||||
if (attachment == null) {
|
||||
return true
|
||||
}
|
||||
|
||||
val contentType = attachment.contentType ?: return false
|
||||
if (!MediaUtil.isImageType(contentType)) {
|
||||
return false
|
||||
}
|
||||
|
||||
val allowedTypes = getAllowedAutoDownloadTypes(context)
|
||||
return NotInCallConstraint.isNotInConnectedCall() && allowedTypes.contains(MediaUtil.getDiscreteMimeType(contentType))
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@WorkerThread
|
||||
fun isAutoDownloadPermitted(context: Context, attachment: DatabaseAttachment?): Boolean {
|
||||
if (attachment == null) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (!isFromTrustedConversation(context, attachment)) {
|
||||
Log.w(TAG, "Not allowing download due to untrusted conversation")
|
||||
return false
|
||||
}
|
||||
|
||||
if (attachment.size <= 0 && attachment.cdn != Cdn.S3) {
|
||||
Log.w(TAG, "Not auto downloading. Attachment has no declared size.")
|
||||
return false
|
||||
}
|
||||
|
||||
val ciphertextSize: ByteSize = AttachmentCipherStreamUtil.getCiphertextLength(PaddingInputStream.getPaddedSize(attachment.size)).bytes
|
||||
|
||||
if (ciphertextSize > MAX_AUTO_DOWNLOAD_SIZE) {
|
||||
Log.w(TAG, "Not auto downloading. Attachment ciphertext size $ciphertextSize exceeds max auto download size $MAX_AUTO_DOWNLOAD_SIZE")
|
||||
return false
|
||||
}
|
||||
|
||||
val allowedTypes = getAllowedAutoDownloadTypes(context)
|
||||
val contentType = attachment.contentType
|
||||
|
||||
return when {
|
||||
MediaUtil.isLongTextType(contentType) -> true
|
||||
attachment.isSticker -> ciphertextSize <= SMALL_ATTACHMENT_SIZE || allowedForType(allowedTypes, "image", "sticker")
|
||||
attachment.voiceNote -> ciphertextSize <= SMALL_ATTACHMENT_SIZE || allowedForType(allowedTypes, "audio", "voice message")
|
||||
attachment.videoGif -> allowedForType(allowedTypes, "image", "video gif")
|
||||
contentType != null && isNonDocumentType(contentType) -> allowedForType(allowedTypes, MediaUtil.getDiscreteMimeType(contentType), contentType)
|
||||
else -> allowedForType(allowedTypes, "documents", "document")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the specified attachment. If its the only attachment for its linked message, the entire
|
||||
* message is deleted.
|
||||
*
|
||||
* @return message record of deleted message if a message is deleted
|
||||
*/
|
||||
@JvmStatic
|
||||
@WorkerThread
|
||||
fun deleteAttachment(attachment: DatabaseAttachment): MessageRecord? {
|
||||
val attachmentId = attachment.attachmentId
|
||||
val mmsId = attachment.mmsId
|
||||
val attachmentCount = attachments.getAttachmentsForMessage(mmsId).size
|
||||
|
||||
if (attachmentCount <= 1) {
|
||||
val deletedMessageRecord = messages.getMessageRecordOrNull(mmsId)
|
||||
messages.deleteMessage(mmsId)
|
||||
return deletedMessageRecord
|
||||
}
|
||||
|
||||
attachments.deleteAttachment(attachmentId)
|
||||
enqueueAttachmentDelete(messages.getMessageRecordOrNull(mmsId), attachment)
|
||||
return null
|
||||
}
|
||||
|
||||
private fun allowedForType(allowedTypes: Set<String>, typeKey: String?, label: String): Boolean {
|
||||
val notInCall = NotInCallConstraint.isNotInConnectedCall()
|
||||
val typeAllowed = typeKey != null && allowedTypes.contains(typeKey)
|
||||
val allowed = notInCall && typeAllowed
|
||||
if (!allowed) {
|
||||
Log.w(TAG, "Not auto downloading $label. inCall: ${!notInCall} allowedType: $typeAllowed")
|
||||
}
|
||||
return allowed
|
||||
}
|
||||
|
||||
private fun isNonDocumentType(contentType: String): Boolean {
|
||||
return MediaUtil.isImageType(contentType) ||
|
||||
MediaUtil.isVideoType(contentType) ||
|
||||
MediaUtil.isAudioType(contentType)
|
||||
}
|
||||
|
||||
private fun getAllowedAutoDownloadTypes(context: Context): Set<String> {
|
||||
return when {
|
||||
NetworkUtil.isConnectedWifi(context) -> TextSecurePreferences.getWifiMediaDownloadAllowed(context)
|
||||
NetworkUtil.isConnectedRoaming(context) -> TextSecurePreferences.getRoamingMediaDownloadAllowed(context)
|
||||
NetworkUtil.isConnectedMobile(context) -> TextSecurePreferences.getMobileMediaDownloadAllowed(context)
|
||||
else -> emptySet()
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private fun isFromTrustedConversation(context: Context, attachment: DatabaseAttachment): Boolean {
|
||||
return try {
|
||||
val message = messages.getMessageRecord(attachment.mmsId)
|
||||
val fromRecipient = message.fromRecipient
|
||||
val toRecipient = threads.getRecipientForThreadId(message.threadId)
|
||||
|
||||
if (toRecipient != null && toRecipient.isGroup) {
|
||||
toRecipient.isProfileSharing || isTrustedIndividual(fromRecipient, message)
|
||||
} else {
|
||||
isTrustedIndividual(fromRecipient, message)
|
||||
}
|
||||
} catch (e: NoSuchMessageException) {
|
||||
Log.w(TAG, "Message could not be found! Assuming not a trusted contact.")
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun isTrustedIndividual(recipient: Recipient, message: MessageRecord): Boolean {
|
||||
return recipient.isSystemContact ||
|
||||
recipient.isProfileSharing ||
|
||||
message.isOutgoing ||
|
||||
recipient.isSelf ||
|
||||
recipient.isReleaseNotes
|
||||
}
|
||||
}
|
||||
@@ -6025,6 +6025,8 @@
|
||||
<!-- Category title for the quality of the media to be sent -->
|
||||
<string name="DataAndStorageSettingsFragment__sent_media_quality">Sent media quality</string>
|
||||
<string name="DataAndStorageSettingsFragment__sending_high_quality_media_will_use_more_data">Sending high quality media will use more data.</string>
|
||||
<!-- Disclaimer shown under the media auto-download settings. Placeholder is a formatted size like "100 KB". -->
|
||||
<string name="DataAndStorageSettingsFragment__voice_messages_and_stickers_under_size_are_always_auto_downloaded">Voice messages and stickers (under %1$s) are always auto-downloaded.</string>
|
||||
<!-- Setting option that can be selected to default media to be sent as high quality by default -->
|
||||
<string name="DataAndStorageSettingsFragment__high">High</string>
|
||||
<!-- Setting option that can be selected to default media to be sent as standard quality by default -->
|
||||
|
||||
@@ -0,0 +1,493 @@
|
||||
package org.thoughtcrime.securesms.util
|
||||
|
||||
import android.content.Context
|
||||
import io.mockk.Runs
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkObject
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.unmockkAll
|
||||
import io.mockk.verify
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.thoughtcrime.securesms.attachments.AttachmentId
|
||||
import org.thoughtcrime.securesms.attachments.Cdn
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
|
||||
import org.thoughtcrime.securesms.database.AttachmentTable
|
||||
import org.thoughtcrime.securesms.database.MessageTable
|
||||
import org.thoughtcrime.securesms.database.NoSuchMessageException
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.ThreadTable
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NotInCallConstraint
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceDeleteSyncJob
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.stickers.StickerLocator
|
||||
|
||||
class AttachmentUtilTest {
|
||||
|
||||
private val context = mockk<Context>(relaxed = true)
|
||||
private val messageTable = mockk<MessageTable>(relaxed = true)
|
||||
private val threadTable = mockk<ThreadTable>(relaxed = true)
|
||||
private val attachmentTable = mockk<AttachmentTable>(relaxed = true)
|
||||
private val fromRecipient = mockk<Recipient>(relaxed = true)
|
||||
private val toRecipient = mockk<Recipient>(relaxed = true)
|
||||
private val messageRecord = mockk<MessageRecord>(relaxed = true)
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
mockkObject(SignalDatabase.Companion)
|
||||
every { SignalDatabase.instance } returns mockk {
|
||||
every { messageTable } returns this@AttachmentUtilTest.messageTable
|
||||
every { threadTable } returns this@AttachmentUtilTest.threadTable
|
||||
every { attachmentTable } returns this@AttachmentUtilTest.attachmentTable
|
||||
}
|
||||
|
||||
mockkStatic(NotInCallConstraint::class)
|
||||
mockkStatic(NetworkUtil::class)
|
||||
mockkStatic(TextSecurePreferences::class)
|
||||
mockkObject(MultiDeviceDeleteSyncJob.Companion)
|
||||
every { MultiDeviceDeleteSyncJob.enqueueAttachmentDelete(any(), any()) } just Runs
|
||||
|
||||
every { NotInCallConstraint.isNotInConnectedCall() } returns true
|
||||
every { NetworkUtil.isConnectedWifi(context) } returns true
|
||||
every { NetworkUtil.isConnectedRoaming(context) } returns false
|
||||
every { NetworkUtil.isConnectedMobile(context) } returns false
|
||||
every { TextSecurePreferences.getWifiMediaDownloadAllowed(context) } returns emptySet()
|
||||
every { TextSecurePreferences.getRoamingMediaDownloadAllowed(context) } returns emptySet()
|
||||
every { TextSecurePreferences.getMobileMediaDownloadAllowed(context) } returns emptySet()
|
||||
|
||||
every { messageTable.getMessageRecord(any()) } returns messageRecord
|
||||
every { messageRecord.fromRecipient } returns fromRecipient
|
||||
every { messageRecord.threadId } returns 1L
|
||||
every { messageRecord.isOutgoing } returns false
|
||||
every { threadTable.getRecipientForThreadId(any()) } returns toRecipient
|
||||
every { toRecipient.isGroup } returns false
|
||||
every { fromRecipient.isSystemContact } returns true
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
unmockkAll()
|
||||
}
|
||||
|
||||
// --- isAutoDownloadPermitted ---
|
||||
|
||||
@Test
|
||||
fun `null attachment returns true`() {
|
||||
assertTrue(AttachmentUtil.isAutoDownloadPermitted(context, null))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `untrusted conversation returns false`() {
|
||||
every { fromRecipient.isSystemContact } returns false
|
||||
every { fromRecipient.isProfileSharing } returns false
|
||||
every { fromRecipient.isSelf } returns false
|
||||
every { fromRecipient.isReleaseNotes } returns false
|
||||
every { messageRecord.isOutgoing } returns false
|
||||
|
||||
assertFalse(AttachmentUtil.isAutoDownloadPermitted(context, attachment()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NoSuchMessageException treats as untrusted`() {
|
||||
every { messageTable.getMessageRecord(any()) } throws NoSuchMessageException("")
|
||||
assertFalse(AttachmentUtil.isAutoDownloadPermitted(context, attachment()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `group with profile sharing is trusted`() {
|
||||
every { toRecipient.isGroup } returns true
|
||||
every { toRecipient.isProfileSharing } returns true
|
||||
every { fromRecipient.isSystemContact } returns false
|
||||
every { TextSecurePreferences.getWifiMediaDownloadAllowed(context) } returns setOf("image")
|
||||
|
||||
assertTrue(AttachmentUtil.isAutoDownloadPermitted(context, attachment()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `group without profile sharing and untrusted individual returns false`() {
|
||||
every { toRecipient.isGroup } returns true
|
||||
every { toRecipient.isProfileSharing } returns false
|
||||
every { fromRecipient.isSystemContact } returns false
|
||||
every { fromRecipient.isProfileSharing } returns false
|
||||
every { fromRecipient.isSelf } returns false
|
||||
every { fromRecipient.isReleaseNotes } returns false
|
||||
every { messageRecord.isOutgoing } returns false
|
||||
|
||||
assertFalse(AttachmentUtil.isAutoDownloadPermitted(context, attachment()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `null toRecipient falls to individual trust`() {
|
||||
every { threadTable.getRecipientForThreadId(any()) } returns null
|
||||
every { TextSecurePreferences.getWifiMediaDownloadAllowed(context) } returns setOf("image")
|
||||
|
||||
assertTrue(AttachmentUtil.isAutoDownloadPermitted(context, attachment()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `profile sharing from recipient is trusted`() {
|
||||
every { fromRecipient.isSystemContact } returns false
|
||||
every { fromRecipient.isProfileSharing } returns true
|
||||
every { TextSecurePreferences.getWifiMediaDownloadAllowed(context) } returns setOf("image")
|
||||
|
||||
assertTrue(AttachmentUtil.isAutoDownloadPermitted(context, attachment()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `video gif blocked when in call`() {
|
||||
val attachment = attachment(videoGif = true, contentType = "video/mp4")
|
||||
every { TextSecurePreferences.getWifiMediaDownloadAllowed(context) } returns setOf("image")
|
||||
every { NotInCallConstraint.isNotInConnectedCall() } returns false
|
||||
|
||||
assertFalse(AttachmentUtil.isAutoDownloadPermitted(context, attachment))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `document blocked when in call`() {
|
||||
val attachment = attachment(contentType = "application/pdf", fileName = "doc.pdf")
|
||||
every { TextSecurePreferences.getWifiMediaDownloadAllowed(context) } returns setOf("documents")
|
||||
every { NotInCallConstraint.isNotInConnectedCall() } returns false
|
||||
assertFalse(AttachmentUtil.isAutoDownloadPermitted(context, attachment))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `group without profile sharing falls to individual trust`() {
|
||||
every { toRecipient.isGroup } returns true
|
||||
every { toRecipient.isProfileSharing } returns false
|
||||
every { fromRecipient.isSystemContact } returns false
|
||||
every { fromRecipient.isProfileSharing } returns false
|
||||
every { messageRecord.isOutgoing } returns true
|
||||
every { TextSecurePreferences.getWifiMediaDownloadAllowed(context) } returns setOf("image")
|
||||
|
||||
assertTrue(AttachmentUtil.isAutoDownloadPermitted(context, attachment()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `outgoing message is trusted`() {
|
||||
every { fromRecipient.isSystemContact } returns false
|
||||
every { messageRecord.isOutgoing } returns true
|
||||
every { TextSecurePreferences.getWifiMediaDownloadAllowed(context) } returns setOf("image")
|
||||
|
||||
assertTrue(AttachmentUtil.isAutoDownloadPermitted(context, attachment()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `self recipient is trusted`() {
|
||||
every { fromRecipient.isSystemContact } returns false
|
||||
every { fromRecipient.isSelf } returns true
|
||||
every { TextSecurePreferences.getWifiMediaDownloadAllowed(context) } returns setOf("image")
|
||||
|
||||
assertTrue(AttachmentUtil.isAutoDownloadPermitted(context, attachment()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `release notes recipient is trusted`() {
|
||||
every { fromRecipient.isSystemContact } returns false
|
||||
every { fromRecipient.isReleaseNotes } returns true
|
||||
every { TextSecurePreferences.getWifiMediaDownloadAllowed(context) } returns setOf("image")
|
||||
|
||||
assertTrue(AttachmentUtil.isAutoDownloadPermitted(context, attachment()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `zero size rejects`() {
|
||||
every { TextSecurePreferences.getWifiMediaDownloadAllowed(context) } returns setOf("image")
|
||||
assertFalse(AttachmentUtil.isAutoDownloadPermitted(context, attachment(size = 0L, contentType = "image/jpeg")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ciphertext over 200MB rejects`() {
|
||||
val huge = attachment(size = 250L * 1024 * 1024, contentType = "image/jpeg")
|
||||
every { TextSecurePreferences.getWifiMediaDownloadAllowed(context) } returns setOf("image")
|
||||
|
||||
assertFalse(AttachmentUtil.isAutoDownloadPermitted(context, huge))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `long text always permitted`() {
|
||||
val attachment = attachment(contentType = "text/x-signal-plain")
|
||||
assertTrue(AttachmentUtil.isAutoDownloadPermitted(context, attachment))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `small sticker always permitted regardless of allowed types or in-call`() {
|
||||
val attachment = attachment(size = 50L * 1024, isSticker = true, contentType = "image/webp")
|
||||
every { NotInCallConstraint.isNotInConnectedCall() } returns false
|
||||
|
||||
assertTrue(AttachmentUtil.isAutoDownloadPermitted(context, attachment))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `large sticker requires image allowed type`() {
|
||||
val attachment = attachment(size = 500L * 1024, isSticker = true, contentType = "image/webp")
|
||||
every { TextSecurePreferences.getWifiMediaDownloadAllowed(context) } returns setOf("image")
|
||||
|
||||
assertTrue(AttachmentUtil.isAutoDownloadPermitted(context, attachment))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `large sticker blocked when in call`() {
|
||||
val attachment = attachment(size = 500L * 1024, isSticker = true, contentType = "image/webp")
|
||||
every { TextSecurePreferences.getWifiMediaDownloadAllowed(context) } returns setOf("image")
|
||||
every { NotInCallConstraint.isNotInConnectedCall() } returns false
|
||||
|
||||
assertFalse(AttachmentUtil.isAutoDownloadPermitted(context, attachment))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `large sticker blocked when image type not allowed`() {
|
||||
val attachment = attachment(size = 500L * 1024, isSticker = true, contentType = "image/webp")
|
||||
every { TextSecurePreferences.getWifiMediaDownloadAllowed(context) } returns setOf("audio")
|
||||
|
||||
assertFalse(AttachmentUtil.isAutoDownloadPermitted(context, attachment))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `small voice note always permitted`() {
|
||||
val attachment = attachment(size = 10L * 1024, voiceNote = true, contentType = "audio/aac", fileName = null)
|
||||
assertTrue(AttachmentUtil.isAutoDownloadPermitted(context, attachment))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unnamed audio without voice note flag uses audio setting`() {
|
||||
val attachment = attachment(size = 10L * 1024, voiceNote = false, contentType = "audio/aac", fileName = null)
|
||||
every { TextSecurePreferences.getWifiMediaDownloadAllowed(context) } returns setOf("audio")
|
||||
assertTrue(AttachmentUtil.isAutoDownloadPermitted(context, attachment))
|
||||
|
||||
every { TextSecurePreferences.getWifiMediaDownloadAllowed(context) } returns emptySet()
|
||||
assertFalse(AttachmentUtil.isAutoDownloadPermitted(context, attachment))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `named audio uses audio setting`() {
|
||||
val attachment = attachment(size = 10L * 1024, voiceNote = false, contentType = "audio/mpeg", fileName = "song.mp3")
|
||||
every { TextSecurePreferences.getWifiMediaDownloadAllowed(context) } returns setOf("audio")
|
||||
assertTrue(AttachmentUtil.isAutoDownloadPermitted(context, attachment))
|
||||
|
||||
every { TextSecurePreferences.getWifiMediaDownloadAllowed(context) } returns emptySet()
|
||||
assertFalse(AttachmentUtil.isAutoDownloadPermitted(context, attachment))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `large voice note requires audio allowed type`() {
|
||||
val attachment = attachment(size = 500L * 1024, voiceNote = true, contentType = "audio/aac", fileName = null)
|
||||
every { TextSecurePreferences.getWifiMediaDownloadAllowed(context) } returns setOf("audio")
|
||||
|
||||
assertTrue(AttachmentUtil.isAutoDownloadPermitted(context, attachment))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `large voice note blocked when audio not allowed`() {
|
||||
val attachment = attachment(size = 500L * 1024, voiceNote = true, contentType = "audio/aac", fileName = null)
|
||||
every { TextSecurePreferences.getWifiMediaDownloadAllowed(context) } returns setOf("image")
|
||||
|
||||
assertFalse(AttachmentUtil.isAutoDownloadPermitted(context, attachment))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `large voice note blocked when in call`() {
|
||||
val attachment = attachment(size = 500L * 1024, voiceNote = true, contentType = "audio/aac", fileName = null)
|
||||
every { TextSecurePreferences.getWifiMediaDownloadAllowed(context) } returns setOf("audio")
|
||||
every { NotInCallConstraint.isNotInConnectedCall() } returns false
|
||||
|
||||
assertFalse(AttachmentUtil.isAutoDownloadPermitted(context, attachment))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `video gif requires image allowed type`() {
|
||||
val attachment = attachment(videoGif = true, contentType = "video/mp4")
|
||||
every { TextSecurePreferences.getWifiMediaDownloadAllowed(context) } returns setOf("image")
|
||||
assertTrue(AttachmentUtil.isAutoDownloadPermitted(context, attachment))
|
||||
|
||||
every { TextSecurePreferences.getWifiMediaDownloadAllowed(context) } returns emptySet()
|
||||
assertFalse(AttachmentUtil.isAutoDownloadPermitted(context, attachment))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `image content type uses image setting`() {
|
||||
val attachment = attachment(contentType = "image/jpeg")
|
||||
every { TextSecurePreferences.getWifiMediaDownloadAllowed(context) } returns setOf("image")
|
||||
assertTrue(AttachmentUtil.isAutoDownloadPermitted(context, attachment))
|
||||
|
||||
every { TextSecurePreferences.getWifiMediaDownloadAllowed(context) } returns emptySet()
|
||||
assertFalse(AttachmentUtil.isAutoDownloadPermitted(context, attachment))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `video content type uses video setting`() {
|
||||
val attachment = attachment(contentType = "video/mp4")
|
||||
every { TextSecurePreferences.getWifiMediaDownloadAllowed(context) } returns setOf("video")
|
||||
assertTrue(AttachmentUtil.isAutoDownloadPermitted(context, attachment))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `document content type uses documents setting`() {
|
||||
val attachment = attachment(contentType = "application/pdf", fileName = "doc.pdf")
|
||||
every { TextSecurePreferences.getWifiMediaDownloadAllowed(context) } returns setOf("documents")
|
||||
assertTrue(AttachmentUtil.isAutoDownloadPermitted(context, attachment))
|
||||
|
||||
every { TextSecurePreferences.getWifiMediaDownloadAllowed(context) } returns emptySet()
|
||||
assertFalse(AttachmentUtil.isAutoDownloadPermitted(context, attachment))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `null content type falls to documents setting`() {
|
||||
val attachment = attachment(contentType = null, fileName = "file")
|
||||
every { TextSecurePreferences.getWifiMediaDownloadAllowed(context) } returns setOf("documents")
|
||||
assertTrue(AttachmentUtil.isAutoDownloadPermitted(context, attachment))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `in-call blocks non-sticker paths`() {
|
||||
every { NotInCallConstraint.isNotInConnectedCall() } returns false
|
||||
every { TextSecurePreferences.getWifiMediaDownloadAllowed(context) } returns setOf("image", "video", "audio", "documents")
|
||||
|
||||
assertFalse(AttachmentUtil.isAutoDownloadPermitted(context, attachment(contentType = "image/jpeg")))
|
||||
assertFalse(AttachmentUtil.isAutoDownloadPermitted(context, attachment(contentType = "application/pdf", fileName = "a.pdf")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `roaming network uses roaming allowed set`() {
|
||||
every { NetworkUtil.isConnectedWifi(context) } returns false
|
||||
every { NetworkUtil.isConnectedRoaming(context) } returns true
|
||||
every { TextSecurePreferences.getRoamingMediaDownloadAllowed(context) } returns setOf("image")
|
||||
|
||||
assertTrue(AttachmentUtil.isAutoDownloadPermitted(context, attachment(contentType = "image/jpeg")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `mobile network uses mobile allowed set`() {
|
||||
every { NetworkUtil.isConnectedWifi(context) } returns false
|
||||
every { NetworkUtil.isConnectedRoaming(context) } returns false
|
||||
every { NetworkUtil.isConnectedMobile(context) } returns true
|
||||
every { TextSecurePreferences.getMobileMediaDownloadAllowed(context) } returns setOf("image")
|
||||
|
||||
assertTrue(AttachmentUtil.isAutoDownloadPermitted(context, attachment(contentType = "image/jpeg")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `no network yields empty allowed set`() {
|
||||
every { NetworkUtil.isConnectedWifi(context) } returns false
|
||||
every { NetworkUtil.isConnectedRoaming(context) } returns false
|
||||
every { NetworkUtil.isConnectedMobile(context) } returns false
|
||||
|
||||
assertFalse(AttachmentUtil.isAutoDownloadPermitted(context, attachment(contentType = "image/jpeg")))
|
||||
}
|
||||
|
||||
// --- deleteAttachment ---
|
||||
|
||||
@Test
|
||||
fun `deleteAttachment with single attachment deletes message and returns its record`() {
|
||||
val attachment = attachment()
|
||||
every { attachmentTable.getAttachmentsForMessage(42L) } returns listOf(attachment)
|
||||
every { messageTable.getMessageRecordOrNull(42L) } returns messageRecord
|
||||
|
||||
val result = AttachmentUtil.deleteAttachment(attachment)
|
||||
|
||||
assertTrue(result === messageRecord)
|
||||
verify { messageTable.deleteMessage(42L) }
|
||||
verify(exactly = 0) { attachmentTable.deleteAttachment(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deleteAttachment with multiple attachments deletes only the attachment and returns null`() {
|
||||
val attachment = attachment()
|
||||
every { attachmentTable.getAttachmentsForMessage(42L) } returns listOf(attachment, attachment)
|
||||
every { messageTable.getMessageRecordOrNull(42L) } returns messageRecord
|
||||
|
||||
val result = AttachmentUtil.deleteAttachment(attachment)
|
||||
|
||||
assertTrue(result == null)
|
||||
verify { attachmentTable.deleteAttachment(AttachmentId(1L)) }
|
||||
verify(exactly = 0) { messageTable.deleteMessage(any()) }
|
||||
}
|
||||
|
||||
// --- isRestoreOnOpenPermitted ---
|
||||
|
||||
@Test
|
||||
fun `restore null attachment returns true`() {
|
||||
assertTrue(AttachmentUtil.isRestoreOnOpenPermitted(context, null))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `restore null contentType returns false`() {
|
||||
assertFalse(AttachmentUtil.isRestoreOnOpenPermitted(context, attachment(contentType = null, fileName = null)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `restore non-image returns false`() {
|
||||
assertFalse(AttachmentUtil.isRestoreOnOpenPermitted(context, attachment(contentType = "video/mp4")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `restore image permitted when allowed and not in call`() {
|
||||
every { TextSecurePreferences.getWifiMediaDownloadAllowed(context) } returns setOf("image")
|
||||
assertTrue(AttachmentUtil.isRestoreOnOpenPermitted(context, attachment(contentType = "image/jpeg")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `restore image blocked when in call`() {
|
||||
every { TextSecurePreferences.getWifiMediaDownloadAllowed(context) } returns setOf("image")
|
||||
every { NotInCallConstraint.isNotInConnectedCall() } returns false
|
||||
assertFalse(AttachmentUtil.isRestoreOnOpenPermitted(context, attachment(contentType = "image/jpeg")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `restore image blocked when type not allowed`() {
|
||||
every { TextSecurePreferences.getWifiMediaDownloadAllowed(context) } returns emptySet()
|
||||
assertFalse(AttachmentUtil.isRestoreOnOpenPermitted(context, attachment(contentType = "image/jpeg")))
|
||||
}
|
||||
|
||||
private fun attachment(
|
||||
size: Long = 1_000L,
|
||||
contentType: String? = "image/jpeg",
|
||||
isSticker: Boolean = false,
|
||||
voiceNote: Boolean = false,
|
||||
videoGif: Boolean = false,
|
||||
fileName: String? = "photo.jpg"
|
||||
): DatabaseAttachment {
|
||||
return DatabaseAttachment(
|
||||
attachmentId = AttachmentId(1L),
|
||||
mmsId = 42L,
|
||||
hasData = false,
|
||||
hasThumbnail = false,
|
||||
contentType = contentType,
|
||||
transferProgress = AttachmentTable.TRANSFER_PROGRESS_PENDING,
|
||||
size = size,
|
||||
fileName = fileName,
|
||||
cdn = Cdn.CDN_3,
|
||||
location = null,
|
||||
key = null,
|
||||
digest = null,
|
||||
incrementalDigest = null,
|
||||
incrementalMacChunkSize = 0,
|
||||
fastPreflightId = null,
|
||||
voiceNote = voiceNote,
|
||||
borderless = false,
|
||||
videoGif = videoGif,
|
||||
width = 0,
|
||||
height = 0,
|
||||
quote = false,
|
||||
caption = null,
|
||||
stickerLocator = if (isSticker) StickerLocator("pack", "key", 0, null) else null,
|
||||
blurHash = null,
|
||||
audioHash = null,
|
||||
transformProperties = null,
|
||||
displayOrder = 0,
|
||||
uploadTimestamp = 0,
|
||||
dataHash = null,
|
||||
archiveCdn = null,
|
||||
thumbnailRestoreState = AttachmentTable.ThumbnailRestoreState.NONE,
|
||||
archiveTransferState = AttachmentTable.ArchiveTransferState.NONE,
|
||||
uuid = null,
|
||||
quoteTargetContentType = null,
|
||||
metadata = null
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user