Refactor and improve attachment deduping logic.

This commit is contained in:
Greyson Parrelli
2024-03-12 13:38:16 -04:00
committed by Cody Henthorne
parent b7ee6bfcb3
commit 6df1a68213
16 changed files with 1150 additions and 565 deletions

View File

@@ -3,13 +3,14 @@ package org.thoughtcrime.securesms.attachments
import android.os.Parcelable
import com.fasterxml.jackson.annotation.JsonProperty
import kotlinx.parcelize.Parcelize
import org.signal.core.util.DatabaseId
@Parcelize
data class AttachmentId(
@JsonProperty("rowId")
@JvmField
val id: Long
) : Parcelable {
) : Parcelable, DatabaseId {
val isValid: Boolean
get() = id >= 0
@@ -17,4 +18,8 @@ data class AttachmentId(
override fun toString(): String {
return "AttachmentId::$id"
}
override fun serialize(): String {
return id.toString()
}
}

View File

@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.attachments
import android.net.Uri
import android.os.Parcel
import androidx.annotation.VisibleForTesting
import org.signal.core.util.Base64.encodeWithPadding
import org.thoughtcrime.securesms.blurhash.BlurHash
import org.thoughtcrime.securesms.database.AttachmentTable
@@ -14,7 +15,8 @@ import org.whispersystems.signalservice.internal.push.DataMessage
import java.util.Optional
class PointerAttachment : Attachment {
private constructor(
@VisibleForTesting
constructor(
contentType: String,
transferState: Int,
size: Long,

View File

@@ -194,7 +194,7 @@ public class FullBackupImporter extends FullBackupBase {
private static void processAttachment(@NonNull Context context, @NonNull AttachmentSecret attachmentSecret, @NonNull SQLiteDatabase db, @NonNull Attachment attachment, BackupRecordInputStream inputStream)
throws IOException
{
File dataFile = AttachmentTable.newFile(context);
File dataFile = AttachmentTable.newDataFile(context);
Pair<byte[], OutputStream> output = ModernEncryptingPartOutputStream.createFor(attachmentSecret, dataFile, false);
boolean isLegacyTable = SqlUtil.tableExists(db, "part");

View File

@@ -49,7 +49,7 @@ class MediaTable internal constructor(context: Context?, databaseHelper: SignalD
${AttachmentTable.TABLE_NAME}.${AttachmentTable.UPLOAD_TIMESTAMP},
${AttachmentTable.TABLE_NAME}.${AttachmentTable.REMOTE_INCREMENTAL_DIGEST},
${AttachmentTable.TABLE_NAME}.${AttachmentTable.REMOTE_INCREMENTAL_DIGEST_CHUNK_SIZE},
${AttachmentTable.TABLE_NAME}.${AttachmentTable.DATA_HASH},
${AttachmentTable.TABLE_NAME}.${AttachmentTable.DATA_HASH_END},
${MessageTable.TABLE_NAME}.${MessageTable.TYPE},
${MessageTable.TABLE_NAME}.${MessageTable.DATE_SENT},
${MessageTable.TABLE_NAME}.${MessageTable.DATE_RECEIVED},
@@ -71,13 +71,7 @@ class MediaTable internal constructor(context: Context?, databaseHelper: SignalD
${MessageTable.VIEW_ONCE} = 0 AND
${MessageTable.STORY_TYPE} = 0 AND
${MessageTable.LATEST_REVISION_ID} IS NULL AND
(
${AttachmentTable.QUOTE} = 0 OR
(
${AttachmentTable.QUOTE} = 1 AND
${AttachmentTable.DATA_HASH} IS NULL
)
) AND
${AttachmentTable.QUOTE} = 0 AND
${AttachmentTable.STICKER_PACK_ID} IS NULL AND
${MessageTable.TABLE_NAME}.${MessageTable.FROM_RECIPIENT_ID} > 0 AND
$THREAD_RECIPIENT_ID > 0

View File

@@ -79,6 +79,7 @@ import org.thoughtcrime.securesms.database.helpers.migration.V218_RecipientPniSi
import org.thoughtcrime.securesms.database.helpers.migration.V219_PniPreKeyStores
import org.thoughtcrime.securesms.database.helpers.migration.V220_PreKeyConstraints
import org.thoughtcrime.securesms.database.helpers.migration.V221_AddReadColumnToCallEventsTable
import org.thoughtcrime.securesms.database.helpers.migration.V222_DataHashRefactor
/**
* Contains all of the database migrations for [SignalDatabase]. Broken into a separate file for cleanliness.
@@ -160,10 +161,11 @@ object SignalDatabaseMigrations {
218 to V218_RecipientPniSignatureVerified,
219 to V219_PniPreKeyStores,
220 to V220_PreKeyConstraints,
221 to V221_AddReadColumnToCallEventsTable
221 to V221_AddReadColumnToCallEventsTable,
222 to V222_DataHashRefactor
)
const val DATABASE_VERSION = 221
const val DATABASE_VERSION = 222
@JvmStatic
fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {

View File

@@ -0,0 +1,25 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.database.helpers.migration
import android.app.Application
import net.zetetic.database.sqlcipher.SQLiteDatabase
/**
* Adds the new data hash columns and indexes.
*/
@Suppress("ClassName")
object V222_DataHashRefactor : SignalDatabaseMigration {
override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
db.execSQL("DROP INDEX attachment_data_hash_index")
db.execSQL("ALTER TABLE attachment DROP COLUMN data_hash")
db.execSQL("ALTER TABLE attachment ADD COLUMN data_hash_start TEXT DEFAULT NULL")
db.execSQL("ALTER TABLE attachment ADD COLUMN data_hash_end TEXT DEFAULT NULL")
db.execSQL("CREATE INDEX attachment_data_hash_start_index ON attachment (data_hash_start)")
db.execSQL("CREATE INDEX attachment_data_hash_end_index ON attachment (data_hash_end)")
}
}

View File

@@ -205,7 +205,7 @@ public final class AttachmentCompressionJob extends BaseJob {
} else if (constraints.canResize(attachment)) {
Log.i(TAG, "Compressing image.");
try (MediaStream converted = compressImage(context, attachment, constraints)) {
attachmentDatabase.updateAttachmentData(attachment, converted, false);
attachmentDatabase.updateAttachmentData(attachment, converted);
}
attachmentDatabase.markAttachmentAsTransformed(attachmentId, false);
} else if (constraints.isSatisfied(context, attachment)) {
@@ -263,7 +263,7 @@ public final class AttachmentCompressionJob extends BaseJob {
Log.i(TAG, "Compressing with streaming muxer");
AttachmentSecret attachmentSecret = AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret();
File file = AttachmentTable.newFile(context);
File file = AttachmentTable.newDataFile(context);
file.deleteOnExit();
boolean faststart = false;
@@ -296,7 +296,7 @@ public final class AttachmentCompressionJob extends BaseJob {
final long plaintextLength = ModernEncryptingPartOutputStream.getPlaintextLength(file.length());
try (MediaStream mediaStream = new MediaStream(postProcessor.process(plaintextLength), MimeTypes.VIDEO_MP4, 0, 0, true)) {
attachmentDatabase.updateAttachmentData(attachment, mediaStream, true);
attachmentDatabase.updateAttachmentData(attachment, mediaStream);
faststart = true;
} catch (VideoPostProcessingException e) {
Log.w(TAG, "Exception thrown during post processing.", e);
@@ -310,7 +310,7 @@ public final class AttachmentCompressionJob extends BaseJob {
if (!faststart) {
try (MediaStream mediaStream = new MediaStream(ModernDecryptingPartInputStream.createFor(attachmentSecret, file, 0), MimeTypes.VIDEO_MP4, 0, 0, false)) {
attachmentDatabase.updateAttachmentData(attachment, mediaStream, true);
attachmentDatabase.updateAttachmentData(attachment, mediaStream);
}
}
} finally {
@@ -339,7 +339,7 @@ public final class AttachmentCompressionJob extends BaseJob {
100,
percent));
}, cancelationSignal)) {
attachmentDatabase.updateAttachmentData(attachment, mediaStream, true);
attachmentDatabase.updateAttachmentData(attachment, mediaStream);
attachmentDatabase.markAttachmentAsTransformed(attachment.attachmentId, mediaStream.getFaststart());
}

View File

@@ -168,7 +168,7 @@ public final class AttachmentDownloadJob extends BaseJob {
if (attachment.cdnNumber != ReleaseChannel.CDN_NUMBER) {
retrieveAttachment(messageId, attachmentId, attachment);
} else {
retrieveUrlAttachment(messageId, attachmentId, attachment);
retrieveAttachmentForReleaseChannel(messageId, attachmentId, attachment);
}
}
@@ -216,7 +216,7 @@ public final class AttachmentDownloadJob extends BaseJob {
return isCanceled();
}
});
database.insertAttachmentsForPlaceholder(messageId, attachmentId, stream);
database.finalizeAttachmentAfterDownload(messageId, attachmentId, stream);
} catch (RangeException e) {
Log.w(TAG, "Range exception, file size " + attachmentFile.length(), e);
if (attachmentFile.delete()) {
@@ -278,9 +278,9 @@ public final class AttachmentDownloadJob extends BaseJob {
}
}
private void retrieveUrlAttachment(long messageId,
final AttachmentId attachmentId,
final Attachment attachment)
private void retrieveAttachmentForReleaseChannel(long messageId,
final AttachmentId attachmentId,
final Attachment attachment)
throws IOException
{
try (Response response = S3.getObject(Objects.requireNonNull(attachment.fileName))) {
@@ -289,7 +289,7 @@ public final class AttachmentDownloadJob extends BaseJob {
if (body.contentLength() > FeatureFlags.maxAttachmentReceiveSizeBytes()) {
throw new MmsException("Attachment too large, failing download");
}
SignalDatabase.attachments().insertAttachmentsForPlaceholder(messageId, attachmentId, Okio.buffer(body.source()).inputStream());
SignalDatabase.attachments().finalizeAttachmentAfterDownload(messageId, attachmentId, Okio.buffer(body.source()).inputStream());
}
} catch (MmsException e) {
Log.w(TAG, "Experienced exception while trying to download an attachment.", e);

View File

@@ -42,6 +42,7 @@ import java.io.IOException
import java.util.Objects
import java.util.Optional
import java.util.concurrent.TimeUnit
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.milliseconds
/**
@@ -60,7 +61,7 @@ class AttachmentUploadJob private constructor(
private val TAG = Log.tag(AttachmentUploadJob::class.java)
private val UPLOAD_REUSE_THRESHOLD = TimeUnit.DAYS.toMillis(3)
val UPLOAD_REUSE_THRESHOLD = 3.days.inWholeMilliseconds
/**
* Foreground notification shows while uploading attachments above this.
@@ -162,7 +163,7 @@ class AttachmentUploadJob private constructor(
buildAttachmentStream(databaseAttachment, notification, uploadSpec!!).use { localAttachment ->
val remoteAttachment = messageSender.uploadAttachment(localAttachment)
val attachment = PointerAttachment.forPointer(Optional.of(remoteAttachment), null, databaseAttachment.fastPreflightId).get()
SignalDatabase.attachments.updateAttachmentAfterUpload(databaseAttachment.attachmentId, attachment, remoteAttachment.uploadTimestamp)
SignalDatabase.attachments.finalizeAttachmentAfterUpload(databaseAttachment.attachmentId, attachment, remoteAttachment.uploadTimestamp)
}
}
} catch (e: NonSuccessfulResumableUploadResponseCodeException) {

View File

@@ -139,7 +139,7 @@ public final class LegacyAttachmentUploadJob extends BaseJob {
SignalServiceAttachmentPointer remoteAttachment = messageSender.uploadAttachment(localAttachment);
Attachment attachment = PointerAttachment.forPointer(Optional.of(remoteAttachment), null, databaseAttachment.fastPreflightId).get();
database.updateAttachmentAfterUpload(databaseAttachment.attachmentId, attachment, remoteAttachment.getUploadTimestamp());
database.finalizeAttachmentAfterUpload(databaseAttachment.attachmentId, attachment, remoteAttachment.getUploadTimestamp());
}
} catch (NonSuccessfulResumableUploadResponseCodeException e) {
if (e.getCode() == 400) {

View File

@@ -64,11 +64,6 @@ public final class StickerSearchRepository {
return out;
}
public @NonNull Single<Boolean> getStickerFeatureAvailability() {
return Single.fromCallable(this::getStickerFeatureAvailabilitySync)
.observeOn(Schedulers.io());
}
public void getStickerFeatureAvailability(@NonNull Callback<Boolean> callback) {
SignalExecutors.BOUNDED.execute(() -> {
callback.onResult(getStickerFeatureAvailabilitySync());