Update to latest backup protos; Bump to libsignal v0.73.1

This commit is contained in:
Cody Henthorne
2025-05-30 14:09:30 -04:00
parent 340b94f849
commit 13ddd067ef
261 changed files with 368 additions and 94 deletions

View File

@@ -15,8 +15,12 @@ import java.util.UUID
class ArchivedAttachment : Attachment {
companion object {
private const val NO_ARCHIVE_CDN = -404
}
@JvmField
val archiveCdn: Int
val archiveCdn: Int?
@JvmField
val archiveMediaName: String
@@ -31,6 +35,7 @@ class ArchivedAttachment : Attachment {
contentType: String?,
size: Long,
cdn: Int,
uploadTimestamp: Long?,
key: ByteArray,
iv: ByteArray?,
cdnKey: String?,
@@ -71,7 +76,7 @@ class ArchivedAttachment : Attachment {
width = width ?: 0,
height = height ?: 0,
incrementalMacChunkSize = incrementalMacChunkSize ?: 0,
uploadTimestamp = 0,
uploadTimestamp = uploadTimestamp ?: 0,
caption = caption,
stickerLocator = stickerLocator,
blurHash = BlurHash.parseOrNull(blurHash),
@@ -79,14 +84,14 @@ class ArchivedAttachment : Attachment {
transformProperties = null,
uuid = uuid
) {
this.archiveCdn = archiveCdn ?: Cdn.CDN_3.cdnNumber
this.archiveCdn = archiveCdn
this.archiveMediaName = archiveMediaName
this.archiveMediaId = archiveMediaId
this.archiveThumbnailMediaId = archiveThumbnailMediaId
}
constructor(parcel: Parcel) : super(parcel) {
archiveCdn = parcel.readInt()
archiveCdn = parcel.readInt().takeIf { it != NO_ARCHIVE_CDN }
archiveMediaName = parcel.readString()!!
archiveMediaId = parcel.readString()!!
archiveThumbnailMediaId = parcel.readString()!!
@@ -94,7 +99,7 @@ class ArchivedAttachment : Attachment {
override fun writeToParcel(dest: Parcel, flags: Int) {
super.writeToParcel(dest, flags)
dest.writeInt(archiveCdn)
dest.writeInt(archiveCdn ?: NO_ARCHIVE_CDN)
dest.writeString(archiveMediaName)
dest.writeString(archiveMediaId)
dest.writeString(archiveThumbnailMediaId)

View File

@@ -14,6 +14,10 @@ import java.util.UUID
class DatabaseAttachment : Attachment {
companion object {
private const val NO_ARCHIVE_CDN = -404
}
@JvmField
val attachmentId: AttachmentId
@@ -27,7 +31,7 @@ class DatabaseAttachment : Attachment {
val dataHash: String?
@JvmField
val archiveCdn: Int
val archiveCdn: Int?
@JvmField
val thumbnailRestoreState: AttachmentTable.ThumbnailRestoreState
@@ -69,7 +73,7 @@ class DatabaseAttachment : Attachment {
displayOrder: Int,
uploadTimestamp: Long,
dataHash: String?,
archiveCdn: Int,
archiveCdn: Int?,
thumbnailRestoreState: AttachmentTable.ThumbnailRestoreState,
archiveTransferState: AttachmentTable.ArchiveTransferState,
uuid: UUID?
@@ -117,7 +121,7 @@ class DatabaseAttachment : Attachment {
hasThumbnail = ParcelUtil.readBoolean(parcel)
mmsId = parcel.readLong()
displayOrder = parcel.readInt()
archiveCdn = parcel.readInt()
archiveCdn = parcel.readInt().takeIf { it != NO_ARCHIVE_CDN }
thumbnailRestoreState = AttachmentTable.ThumbnailRestoreState.deserialize(parcel.readInt())
archiveTransferState = AttachmentTable.ArchiveTransferState.deserialize(parcel.readInt())
}
@@ -130,7 +134,7 @@ class DatabaseAttachment : Attachment {
ParcelUtil.writeBoolean(dest, hasThumbnail)
dest.writeLong(mmsId)
dest.writeInt(displayOrder)
dest.writeInt(archiveCdn)
dest.writeInt(archiveCdn ?: NO_ARCHIVE_CDN)
dest.writeInt(thumbnailRestoreState.value)
dest.writeInt(archiveTransferState.value)
}

View File

@@ -26,7 +26,7 @@ import org.signal.core.util.getAllTableDefinitions
import org.signal.core.util.getAllTriggerDefinitions
import org.signal.core.util.getForeignKeyViolations
import org.signal.core.util.logging.Log
import org.signal.core.util.requireInt
import org.signal.core.util.requireIntOrNull
import org.signal.core.util.requireNonNullBlob
import org.signal.core.util.stream.NonClosingOutputStream
import org.signal.core.util.urlEncode
@@ -1637,12 +1637,10 @@ sealed class ImportResult {
}
/**
* Iterator that reads values from the given cursor. Expects that ARCHIVE_MEDIA_ID and ARCHIVE_CDN are both
* present and non-null in the cursor.
* Iterator that reads values from the given cursor. Expects that REMOTE_DIGEST is present and non-null, and ARCHIVE_CDN is present.
*
* This class does not assume ownership of the cursor. Recommended usage is within a use statement:
*
*
* ```
* databaseCall().use { cursor ->
* val iterator = ArchivedMediaObjectIterator(cursor)
@@ -1661,7 +1659,7 @@ class ArchiveMediaItemIterator(private val cursor: Cursor) : Iterator<ArchiveMed
override fun next(): ArchiveMediaItem {
val digest = cursor.requireNonNullBlob(AttachmentTable.REMOTE_DIGEST)
val cdn = cursor.requireInt(AttachmentTable.ARCHIVE_CDN)
val cdn = cursor.requireIntOrNull(AttachmentTable.ARCHIVE_CDN)
val mediaId = MediaName.fromDigest(digest).toMediaId(SignalStore.backup.mediaRootBackupKey).encode()
val thumbnailMediaId = MediaName.fromDigestForThumbnail(digest).toMediaId(SignalStore.backup.mediaRootBackupKey).encode()

View File

@@ -9,7 +9,9 @@ import android.text.TextUtils
import org.signal.core.util.Base64
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
import org.thoughtcrime.securesms.attachments.InvalidAttachmentException
import org.thoughtcrime.securesms.database.AttachmentTable
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.RemoteConfig
import org.thoughtcrime.securesms.util.Util
import org.whispersystems.signalservice.api.backup.MediaName
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer
@@ -20,6 +22,7 @@ import java.util.Optional
object DatabaseAttachmentArchiveUtil {
@JvmStatic
fun requireMediaName(attachment: DatabaseAttachment): MediaName {
require(isDigestValidated(attachment))
return MediaName.fromDigest(attachment.remoteDigest!!)
}
@@ -28,18 +31,35 @@ object DatabaseAttachmentArchiveUtil {
*/
@JvmStatic
fun requireMediaNameAsString(attachment: DatabaseAttachment): String {
require(isDigestValidated(attachment))
return MediaName.fromDigest(attachment.remoteDigest!!).name
}
@JvmStatic
fun getMediaName(attachment: DatabaseAttachment): MediaName? {
return attachment.remoteDigest?.let { MediaName.fromDigest(it) }
return if (isDigestValidated(attachment)) {
attachment.remoteDigest?.let { MediaName.fromDigest(it) }
} else {
null
}
}
@JvmStatic
fun requireThumbnailMediaName(attachment: DatabaseAttachment): MediaName {
require(isDigestValidated(attachment))
return MediaName.fromDigestForThumbnail(attachment.remoteDigest!!)
}
private fun isDigestValidated(attachment: DatabaseAttachment): Boolean {
return when (attachment.transferState) {
AttachmentTable.TRANSFER_PROGRESS_DONE,
AttachmentTable.TRANSFER_NEEDS_RESTORE,
AttachmentTable.TRANSFER_RESTORE_IN_PROGRESS,
AttachmentTable.TRANSFER_RESTORE_OFFLOADED -> true
else -> false
}
}
}
fun DatabaseAttachment.requireMediaName(): MediaName {
@@ -77,7 +97,7 @@ fun DatabaseAttachment.createArchiveAttachmentPointer(useArchiveCdn: Boolean): S
mediaId = this.requireMediaName().toMediaId(mediaRootBackupKey).encode()
)
id to archiveCdn
id to (archiveCdn ?: RemoteConfig.backupFallbackArchiveCdn)
} else {
if (remoteLocation.isNullOrEmpty()) {
throw InvalidAttachmentException("empty content id")
@@ -131,7 +151,7 @@ fun DatabaseAttachment.createArchiveThumbnailPointer(): SignalServiceAttachmentP
val key = mediaRootBackupKey.deriveThumbnailTransitKey(requireThumbnailMediaName())
val mediaId = mediaRootBackupKey.deriveMediaId(requireThumbnailMediaName()).encode()
SignalServiceAttachmentPointer(
cdnNumber = archiveCdn,
cdnNumber = archiveCdn ?: RemoteConfig.backupFallbackArchiveCdn,
remoteId = SignalServiceAttachmentRemoteId.Backup(
mediaCdnPath = mediaCdnPath,
mediaId = mediaId

View File

@@ -27,6 +27,7 @@ import org.thoughtcrime.securesms.components.settings.app.usernamelinks.Username
import org.thoughtcrime.securesms.conversation.colors.ChatColors
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues
@@ -37,6 +38,8 @@ import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.ProfileUtil
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.signalservice.api.push.UsernameLinkComponents
import org.whispersystems.signalservice.api.storage.IAPSubscriptionId.AppleIAPOriginalTransactionId
import org.whispersystems.signalservice.api.storage.IAPSubscriptionId.GooglePlayBillingPurchaseToken
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
import org.whispersystems.signalservice.api.util.UuidUtil
import org.whispersystems.signalservice.api.util.toByteArray
@@ -61,6 +64,8 @@ object AccountDataArchiveProcessor {
val chatColors = SignalStore.chatColors.chatColors
val chatWallpaper = SignalStore.wallpaper.currentRawWallpaper
val backupSubscriberRecord = db.inAppPaymentSubscriberTable.getBackupsSubscriber()
emitter.emit(
Frame(
account = AccountData(
@@ -98,6 +103,7 @@ object AccountDataArchiveProcessor {
hasSeenGroupStoryEducationSheet = signalStore.storyValues.userHasSeenGroupStoryEducationSheet,
hasCompletedUsernameOnboarding = signalStore.uiHintValues.hasCompletedUsernameOnboarding(),
customChatColors = db.chatColorsTable.getSavedChatColors().toRemoteChatColors(),
optimizeOnDeviceStorage = signalStore.backupValues.optimizeStorage,
defaultChatStyle = ChatStyleConverter.constructRemoteChatStyle(
db = db,
chatColors = chatColors,
@@ -105,7 +111,8 @@ object AccountDataArchiveProcessor {
chatWallpaper = chatWallpaper
)
),
donationSubscriberData = donationSubscriber?.toSubscriberData(signalStore.inAppPaymentValues.isDonationSubscriptionManuallyCancelled())
donationSubscriberData = donationSubscriber?.toSubscriberData(signalStore.inAppPaymentValues.isDonationSubscriptionManuallyCancelled()),
backupsSubscriberData = backupSubscriberRecord?.toIAPSubscriberData()
)
)
)
@@ -148,6 +155,26 @@ object AccountDataArchiveProcessor {
}
}
if (accountData.backupsSubscriberData != null && accountData.backupsSubscriberData.subscriberId.size > 0 && (accountData.backupsSubscriberData.purchaseToken != null || accountData.backupsSubscriberData.originalTransactionId != null)) {
val remoteSubscriberId = SubscriberId.fromBytes(accountData.backupsSubscriberData.subscriberId.toByteArray())
val localSubscriber = InAppPaymentsRepository.getSubscriber(InAppPaymentSubscriberRecord.Type.BACKUP)
val subscriber = InAppPaymentSubscriberRecord(
subscriberId = remoteSubscriberId,
currency = localSubscriber?.currency,
type = InAppPaymentSubscriberRecord.Type.BACKUP,
requiresCancel = localSubscriber?.requiresCancel ?: false,
paymentMethodType = InAppPaymentData.PaymentMethodType.UNKNOWN,
iapSubscriptionId = if (accountData.backupsSubscriberData.purchaseToken != null) {
GooglePlayBillingPurchaseToken(accountData.backupsSubscriberData.purchaseToken)
} else {
AppleIAPOriginalTransactionId(accountData.backupsSubscriberData.originalTransactionId!!)
}
)
InAppPaymentsRepository.setSubscriber(subscriber)
}
if (accountData.avatarUrlPath.isNotEmpty()) {
AppDependencies.jobManager.add(RetrieveProfileAvatarJob(Recipient.self().fresh(), accountData.avatarUrlPath))
}
@@ -184,6 +211,7 @@ object AccountDataArchiveProcessor {
SignalStore.story.isFeatureDisabled = settings.storiesDisabled
SignalStore.story.userHasSeenGroupStoryEducationSheet = settings.hasSeenGroupStoryEducationSheet
SignalStore.story.viewedReceiptsEnabled = settings.storyViewReceiptsEnabled ?: settings.readReceipts
SignalStore.backup.optimizeStorage = settings.optimizeOnDeviceStorage
settings.customChatColors
.mapNotNull { chatColor ->
@@ -288,6 +316,23 @@ object AccountDataArchiveProcessor {
return AccountData.SubscriberData(subscriberId = subscriberId, currencyCode = currencyCode, manuallyCancelled = manuallyCancelled)
}
private fun InAppPaymentSubscriberRecord?.toIAPSubscriberData(): AccountData.IAPSubscriberData? {
if (this == null) {
return null
}
val builder = AccountData.IAPSubscriberData.Builder()
.subscriberId(this.subscriberId.bytes.toByteString())
if (this.iapSubscriptionId?.purchaseToken != null) {
builder.purchaseToken(this.iapSubscriptionId.purchaseToken)
} else if (this.iapSubscriptionId?.originalTransactionId != null) {
builder.originalTransactionId(this.iapSubscriptionId.originalTransactionId)
}
return builder.build()
}
private fun List<ChatColors>.toRemoteChatColors(): List<ChatStyle.CustomChatColor> {
return this
.mapNotNull { local ->

View File

@@ -9,6 +9,7 @@ import okio.ByteString
import okio.ByteString.Companion.toByteString
import org.signal.core.util.Base64
import org.signal.core.util.emptyIfNull
import org.signal.core.util.isNotNullOrBlank
import org.signal.core.util.nullIfBlank
import org.signal.core.util.orNull
import org.thoughtcrime.securesms.attachments.ArchivedAttachment
@@ -45,19 +46,22 @@ fun FilePointer?.toLocalAttachment(
uuid: ByteString? = null,
quote: Boolean = false
): Attachment? {
if (this == null) return null
if (this == null || this.locatorInfo == null) return null
if (this.attachmentLocator != null) {
val hasMediaName = this.locatorInfo.mediaName.isNotEmpty()
val hasTransitInfo = this.locatorInfo.transitCdnKey != null
if (hasTransitInfo && !hasMediaName) {
val signalAttachmentPointer = SignalServiceAttachmentPointer(
cdnNumber = this.attachmentLocator.cdnNumber,
remoteId = SignalServiceAttachmentRemoteId.from(attachmentLocator.cdnKey),
cdnNumber = this.locatorInfo.transitCdnNumber ?: Cdn.CDN_0.cdnNumber,
remoteId = SignalServiceAttachmentRemoteId.from(locatorInfo.transitCdnKey),
contentType = contentType,
key = this.attachmentLocator.key.toByteArray(),
size = Optional.ofNullable(attachmentLocator.size),
key = this.locatorInfo.key.toByteArray(),
size = Optional.ofNullable(locatorInfo.size),
preview = Optional.empty(),
width = this.width ?: 0,
height = this.height ?: 0,
digest = Optional.ofNullable(this.attachmentLocator.digest.toByteArray()),
digest = Optional.ofNullable(this.locatorInfo.digest.toByteArray()),
incrementalDigest = Optional.ofNullable(this.incrementalMac?.toByteArray()),
incrementalMacChunkSize = this.incrementalMacChunkSize ?: 0,
fileName = Optional.ofNullable(fileName),
@@ -66,7 +70,7 @@ fun FilePointer?.toLocalAttachment(
isGif = gif,
caption = Optional.ofNullable(this.caption),
blurHash = Optional.ofNullable(this.blurHash),
uploadTimestamp = this.attachmentLocator.uploadTimestamp?.clampToValidBackupRange() ?: 0,
uploadTimestamp = this.locatorInfo.transitTierUploadTimestamp?.clampToValidBackupRange() ?: 0,
uuid = UuidUtil.fromByteStringOrNull(uuid)
)
return PointerAttachment.forPointer(
@@ -74,7 +78,7 @@ fun FilePointer?.toLocalAttachment(
stickerLocator = stickerLocator,
transferState = if (wasDownloaded) AttachmentTable.TRANSFER_NEEDS_RESTORE else AttachmentTable.TRANSFER_PROGRESS_PENDING
).orNull()
} else if (this.invalidAttachmentLocator != null) {
} else if (!hasMediaName) {
return TombstoneAttachment(
contentType = contentType,
incrementalMac = this.incrementalMac?.toByteArray(),
@@ -91,19 +95,20 @@ fun FilePointer?.toLocalAttachment(
stickerLocator = stickerLocator,
uuid = UuidUtil.fromByteStringOrNull(uuid)
)
} else if (this.backupLocator != null) {
} else {
return ArchivedAttachment(
contentType = contentType,
size = this.backupLocator.size.toLong(),
cdn = this.backupLocator.transitCdnNumber ?: Cdn.CDN_0.cdnNumber,
key = this.backupLocator.key.toByteArray(),
size = this.locatorInfo.size.toLong(),
cdn = this.locatorInfo.transitCdnNumber ?: Cdn.CDN_0.cdnNumber,
uploadTimestamp = this.locatorInfo.transitTierUploadTimestamp ?: 0,
key = this.locatorInfo.key.toByteArray(),
iv = null,
cdnKey = this.backupLocator.transitCdnKey?.nullIfBlank(),
archiveCdn = this.backupLocator.cdnNumber,
archiveMediaName = this.backupLocator.mediaName,
archiveMediaId = importState.mediaRootBackupKey.deriveMediaId(MediaName(this.backupLocator.mediaName)).encode(),
archiveThumbnailMediaId = importState.mediaRootBackupKey.deriveMediaId(MediaName.forThumbnailFromMediaName(this.backupLocator.mediaName)).encode(),
digest = this.backupLocator.digest.toByteArray(),
cdnKey = this.locatorInfo.transitCdnKey?.nullIfBlank(),
archiveCdn = this.locatorInfo.mediaTierCdnNumber,
archiveMediaName = this.locatorInfo.mediaName,
archiveMediaId = importState.mediaRootBackupKey.deriveMediaId(MediaName(this.locatorInfo.mediaName)).encode(),
archiveThumbnailMediaId = importState.mediaRootBackupKey.deriveMediaId(MediaName.forThumbnailFromMediaName(this.locatorInfo.mediaName)).encode(),
digest = this.locatorInfo.digest.toByteArray(),
incrementalMac = this.incrementalMac?.toByteArray(),
incrementalMacChunkSize = this.incrementalMacChunkSize,
width = this.width,
@@ -119,7 +124,6 @@ fun FilePointer?.toLocalAttachment(
fileName = fileName
)
}
return null
}
/**
@@ -136,49 +140,85 @@ fun DatabaseAttachment.toRemoteFilePointer(mediaArchiveEnabled: Boolean, content
builder.caption = this.caption
builder.blurHash = this.blurHash?.hash
if (this.remoteKey.isNullOrBlank() || this.remoteDigest == null || this.size == 0L) {
builder.invalidAttachmentLocator = FilePointer.InvalidAttachmentLocator()
return builder.build()
builder.setLegacyLocators(this, mediaArchiveEnabled)
builder.locatorInfo = this.toLocatorInfo()
return builder.build()
}
fun FilePointer.Builder.setLegacyLocators(attachment: DatabaseAttachment, mediaArchiveEnabled: Boolean) {
if (attachment.remoteKey.isNullOrBlank() || attachment.remoteDigest == null || attachment.size == 0L) {
this.invalidAttachmentLocator = FilePointer.InvalidAttachmentLocator()
return
}
if (this.transferState == AttachmentTable.TRANSFER_PROGRESS_PERMANENT_FAILURE && this.archiveTransferState != AttachmentTable.ArchiveTransferState.FINISHED) {
builder.invalidAttachmentLocator = FilePointer.InvalidAttachmentLocator()
return builder.build()
if (attachment.transferState == AttachmentTable.TRANSFER_PROGRESS_PERMANENT_FAILURE && attachment.archiveTransferState != AttachmentTable.ArchiveTransferState.FINISHED) {
this.invalidAttachmentLocator = FilePointer.InvalidAttachmentLocator()
return
}
val pending = this.archiveTransferState != AttachmentTable.ArchiveTransferState.FINISHED && (this.transferState != AttachmentTable.TRANSFER_PROGRESS_DONE && this.transferState != AttachmentTable.TRANSFER_RESTORE_OFFLOADED)
val pending = attachment.archiveTransferState != AttachmentTable.ArchiveTransferState.FINISHED && (attachment.transferState != AttachmentTable.TRANSFER_PROGRESS_DONE && attachment.transferState != AttachmentTable.TRANSFER_RESTORE_OFFLOADED)
if (mediaArchiveEnabled && !pending) {
val transitCdnKey = this.remoteLocation?.nullIfBlank()
val transitCdnNumber = this.cdn.cdnNumber.takeIf { transitCdnKey != null }
val archiveMediaName = this.getMediaName()?.toString()
val transitCdnKey = attachment.remoteLocation?.nullIfBlank()
val transitCdnNumber = attachment.cdn.cdnNumber.takeIf { transitCdnKey != null }
val archiveMediaName = attachment.getMediaName()?.toString()
builder.backupLocator = FilePointer.BackupLocator(
this.backupLocator = FilePointer.BackupLocator(
mediaName = archiveMediaName.emptyIfNull(),
cdnNumber = this.archiveCdn.takeIf { archiveMediaName != null },
key = Base64.decode(remoteKey).toByteString(),
size = this.size.toInt(),
digest = this.remoteDigest.toByteString(),
cdnNumber = attachment.archiveCdn.takeIf { archiveMediaName != null },
key = Base64.decode(attachment.remoteKey).toByteString(),
size = attachment.size.toInt(),
digest = attachment.remoteDigest.toByteString(),
transitCdnNumber = transitCdnNumber,
transitCdnKey = transitCdnKey
)
return builder.build()
return
}
if (this.remoteLocation.isNullOrBlank()) {
builder.invalidAttachmentLocator = FilePointer.InvalidAttachmentLocator()
return builder.build()
if (attachment.remoteLocation.isNullOrBlank()) {
this.invalidAttachmentLocator = FilePointer.InvalidAttachmentLocator()
return
}
builder.attachmentLocator = FilePointer.AttachmentLocator(
cdnKey = this.remoteLocation,
cdnNumber = this.cdn.cdnNumber,
uploadTimestamp = this.uploadTimestamp.takeIf { it > 0 }?.clampToValidBackupRange(),
key = Base64.decode(remoteKey).toByteString(),
size = this.size.toInt(),
digest = this.remoteDigest.toByteString()
this.attachmentLocator = FilePointer.AttachmentLocator(
cdnKey = attachment.remoteLocation,
cdnNumber = attachment.cdn.cdnNumber,
uploadTimestamp = attachment.uploadTimestamp.takeIf { it > 0 }?.clampToValidBackupRange(),
key = Base64.decode(attachment.remoteKey).toByteString(),
size = attachment.size.toInt(),
digest = attachment.remoteDigest.toByteString()
)
return builder.build()
}
fun DatabaseAttachment.toLocatorInfo(): FilePointer.LocatorInfo {
if (this.remoteKey.isNullOrBlank() || this.remoteDigest == null || this.size == 0L) {
return FilePointer.LocatorInfo()
}
if (this.transferState == AttachmentTable.TRANSFER_PROGRESS_PERMANENT_FAILURE && this.archiveTransferState != AttachmentTable.ArchiveTransferState.FINISHED) {
return FilePointer.LocatorInfo()
}
val locatorBuilder = FilePointer.LocatorInfo.Builder()
val remoteKey = Base64.decode(this.remoteKey).toByteString()
val archiveMediaName = this.getMediaName()?.toString()
locatorBuilder.key = remoteKey
locatorBuilder.digest = this.remoteDigest.toByteString()
locatorBuilder.size = this.size.toInt()
if (this.remoteLocation.isNotNullOrBlank()) {
locatorBuilder.transitCdnKey = this.remoteLocation
locatorBuilder.transitCdnNumber = this.cdn.cdnNumber
locatorBuilder.transitTierUploadTimestamp = this.uploadTimestamp.takeIf { it > 0 }?.clampToValidBackupRange()
}
locatorBuilder.mediaTierCdnNumber = this.archiveCdn?.takeIf { archiveMediaName != null }
locatorBuilder.mediaName = archiveMediaName.emptyIfNull()
return locatorBuilder.build()
}
fun Long.clampToValidBackupRange(): Long {

View File

@@ -54,6 +54,7 @@ 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.requireIntOrNull
import org.signal.core.util.requireLong
import org.signal.core.util.requireNonNullBlob
import org.signal.core.util.requireNonNullString
@@ -264,7 +265,7 @@ class AttachmentTable(
$UPLOAD_TIMESTAMP INTEGER DEFAULT 0,
$DATA_HASH_START TEXT DEFAULT NULL,
$DATA_HASH_END TEXT DEFAULT NULL,
$ARCHIVE_CDN INTEGER DEFAULT 0,
$ARCHIVE_CDN INTEGER DEFAULT NULL,
$ARCHIVE_TRANSFER_FILE TEXT DEFAULT NULL,
$ARCHIVE_TRANSFER_STATE INTEGER DEFAULT ${ArchiveTransferState.NONE.value},
$THUMBNAIL_FILE TEXT DEFAULT NULL,
@@ -705,7 +706,7 @@ class AttachmentTable(
.update(TABLE_NAME)
.values(
ARCHIVE_TRANSFER_STATE to ArchiveTransferState.NONE.value,
ARCHIVE_CDN to 0
ARCHIVE_CDN to null
)
.where("$REMOTE_DIGEST = ?", digest)
.run()
@@ -1977,7 +1978,7 @@ class AttachmentTable(
displayOrder = jsonObject.getInt(DISPLAY_ORDER),
uploadTimestamp = jsonObject.getLong(UPLOAD_TIMESTAMP),
dataHash = jsonObject.getString(DATA_HASH_END),
archiveCdn = jsonObject.getInt(ARCHIVE_CDN),
archiveCdn = if (jsonObject.isNull(ARCHIVE_CDN)) null else jsonObject.getInt(ARCHIVE_CDN),
thumbnailRestoreState = ThumbnailRestoreState.deserialize(jsonObject.getInt(THUMBNAIL_RESTORE_STATE)),
archiveTransferState = ArchiveTransferState.deserialize(jsonObject.getInt(ARCHIVE_TRANSFER_STATE)),
uuid = UuidUtil.parseOrNull(jsonObject.getString(ATTACHMENT_UUID))
@@ -2064,7 +2065,7 @@ class AttachmentTable(
writableDatabase
.update(TABLE_NAME)
.values(
ARCHIVE_CDN to 0
ARCHIVE_CDN to null
)
.where(query.where, query.whereArgs)
.run()
@@ -2075,7 +2076,7 @@ class AttachmentTable(
writableDatabase
.updateAll(TABLE_NAME)
.values(
ARCHIVE_CDN to 0,
ARCHIVE_CDN to null,
ARCHIVE_TRANSFER_STATE to ArchiveTransferState.NONE.value
)
.run()
@@ -2630,7 +2631,7 @@ class AttachmentTable(
displayOrder = cursor.requireInt(DISPLAY_ORDER),
uploadTimestamp = cursor.requireLong(UPLOAD_TIMESTAMP),
dataHash = cursor.requireString(DATA_HASH_END),
archiveCdn = cursor.requireInt(ARCHIVE_CDN),
archiveCdn = cursor.requireIntOrNull(ARCHIVE_CDN),
thumbnailRestoreState = ThumbnailRestoreState.deserialize(cursor.requireInt(THUMBNAIL_RESTORE_STATE)),
archiveTransferState = ArchiveTransferState.deserialize(cursor.requireInt(ARCHIVE_TRANSFER_STATE)),
uuid = UuidUtil.parseOrNull(cursor.requireString(ATTACHMENT_UUID))
@@ -2658,7 +2659,7 @@ class AttachmentTable(
hashEnd = this.requireString(DATA_HASH_END),
transformProperties = TransformProperties.parse(this.requireString(TRANSFORM_PROPERTIES)),
uploadTimestamp = this.requireLong(UPLOAD_TIMESTAMP),
archiveCdn = this.requireInt(ARCHIVE_CDN),
archiveCdn = this.requireIntOrNull(ARCHIVE_CDN),
archiveTransferState = this.requireInt(ARCHIVE_TRANSFER_STATE),
thumbnailFile = this.requireString(THUMBNAIL_FILE),
thumbnailRandom = this.requireBlob(THUMBNAIL_RANDOM),
@@ -2726,7 +2727,7 @@ class AttachmentTable(
val hashEnd: String?,
val transformProperties: TransformProperties,
val uploadTimestamp: Long,
val archiveCdn: Int,
val archiveCdn: Int?,
val archiveTransferState: Int,
val thumbnailFile: String?,
val thumbnailRandom: ByteArray?,

View File

@@ -15,6 +15,7 @@ import org.signal.core.util.readToList
import org.signal.core.util.readToSet
import org.signal.core.util.requireBoolean
import org.signal.core.util.requireInt
import org.signal.core.util.requireIntOrNull
import org.signal.core.util.requireNonNullBlob
import org.signal.core.util.requireNonNullString
import org.signal.core.util.select
@@ -257,7 +258,7 @@ class BackupMediaSnapshotTable(context: Context, database: SignalDatabase) : Dat
class ArchiveMediaItem(
val mediaId: String,
val thumbnailMediaId: String,
val cdn: Int,
val cdn: Int?,
val digest: ByteArray
)
@@ -268,7 +269,7 @@ class BackupMediaSnapshotTable(context: Context, database: SignalDatabase) : Dat
class MediaEntry(
val mediaId: String,
val cdn: Int,
val cdn: Int?,
val digest: ByteArray,
val isThumbnail: Boolean
) {
@@ -276,7 +277,7 @@ class BackupMediaSnapshotTable(context: Context, database: SignalDatabase) : Dat
fun fromCursor(cursor: Cursor): MediaEntry {
return MediaEntry(
mediaId = cursor.requireNonNullString(MEDIA_ID),
cdn = cursor.requireInt(CDN),
cdn = cursor.requireIntOrNull(CDN),
digest = cursor.requireNonNullBlob(REMOTE_DIGEST),
isThumbnail = cursor.requireBoolean(IS_THUMBNAIL)
)

View File

@@ -130,6 +130,7 @@ import org.thoughtcrime.securesms.database.helpers.migration.V272_UpdateUnreadCo
import org.thoughtcrime.securesms.database.helpers.migration.V273_FixUnreadOriginalMessages
import org.thoughtcrime.securesms.database.helpers.migration.V274_BackupMediaSnapshotLastSeenOnRemote
import org.thoughtcrime.securesms.database.helpers.migration.V275_EnsureDefaultAllChatsFolder
import org.thoughtcrime.securesms.database.helpers.migration.V276_AttachmentCdnDefaultValueMigration
import org.thoughtcrime.securesms.database.SQLiteDatabase as SignalSqliteDatabase
/**
@@ -265,10 +266,11 @@ object SignalDatabaseMigrations {
272 to V272_UpdateUnreadCountIndices,
273 to V273_FixUnreadOriginalMessages,
274 to V274_BackupMediaSnapshotLastSeenOnRemote,
275 to V275_EnsureDefaultAllChatsFolder
275 to V275_EnsureDefaultAllChatsFolder,
276 to V276_AttachmentCdnDefaultValueMigration
)
const val DATABASE_VERSION = 275
const val DATABASE_VERSION = 276
@JvmStatic
fun migrate(context: Application, db: SignalSqliteDatabase, oldVersion: Int, newVersion: Int) {

View File

@@ -0,0 +1,108 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.database.helpers.migration
import android.app.Application
import org.signal.core.util.Stopwatch
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.database.SQLiteDatabase
/**
* We want to be able to distinguish between an unset CDN (null) and CDN 0. But we default the current CDN values to zero.
* This migration updates things so that the CDN columns default to null. We also consider all current CDN 0's to actually be unset values.
*/
object V276_AttachmentCdnDefaultValueMigration : SignalDatabaseMigration {
private val TAG = Log.tag(V276_AttachmentCdnDefaultValueMigration::class)
override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
val stopwatch = Stopwatch("v276")
db.execSQL("UPDATE attachment SET archive_cdn = NULL WHERE archive_cdn = 0")
stopwatch.split("fix-old-data")
db.execSQL(
"""
CREATE TABLE attachment_tmp (
_id INTEGER PRIMARY KEY AUTOINCREMENT,
message_id INTEGER,
content_type TEXT,
remote_key TEXT,
remote_location TEXT,
remote_digest BLOB,
remote_incremental_digest BLOB,
remote_incremental_digest_chunk_size INTEGER DEFAULT 0,
cdn_number INTEGER DEFAULT 0,
transfer_state INTEGER,
transfer_file TEXT DEFAULT NULL,
data_file TEXT,
data_size INTEGER,
data_random BLOB,
file_name TEXT,
fast_preflight_id TEXT,
voice_note INTEGER DEFAULT 0,
borderless INTEGER DEFAULT 0,
video_gif INTEGER DEFAULT 0,
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,
blur_hash TEXT DEFAULT NULL,
transform_properties TEXT DEFAULT NULL,
display_order INTEGER DEFAULT 0,
upload_timestamp INTEGER DEFAULT 0,
data_hash_start TEXT DEFAULT NULL,
data_hash_end TEXT DEFAULT NULL,
archive_cdn INTEGER DEFAULT NULL,
archive_transfer_file TEXT DEFAULT NULL,
archive_transfer_state INTEGER DEFAULT 0,
thumbnail_file TEXT DEFAULT NULL,
thumbnail_random BLOB DEFAULT NULL,
thumbnail_restore_state INTEGER DEFAULT 0,
attachment_uuid TEXT DEFAULT NULL,
remote_iv BLOB DEFAULT NULL,
offload_restored_at INTEGER DEFAULT 0
)
"""
)
stopwatch.split("create-new-table")
db.execSQL("INSERT INTO attachment_tmp SELECT * FROM attachment")
stopwatch.split("copy-data")
db.execSQL("DROP TABLE attachment")
stopwatch.split("drop-table")
db.execSQL("ALTER TABLE attachment_tmp RENAME TO attachment")
stopwatch.split("rename-table")
db.execSQL("CREATE INDEX IF NOT EXISTS attachment_message_id_index ON attachment (message_id);")
db.execSQL("CREATE INDEX IF NOT EXISTS attachment_transfer_state_index ON attachment (transfer_state);")
db.execSQL("CREATE INDEX IF NOT EXISTS attachment_sticker_pack_id_index ON attachment (sticker_pack_id);")
db.execSQL("CREATE INDEX IF NOT EXISTS attachment_data_hash_start_index ON attachment (data_hash_start);")
db.execSQL("CREATE INDEX IF NOT EXISTS attachment_data_hash_end_index ON attachment (data_hash_end);")
db.execSQL("CREATE INDEX IF NOT EXISTS attachment_data_index ON attachment (data_file);")
db.execSQL("CREATE INDEX IF NOT EXISTS attachment_archive_transfer_state ON attachment (archive_transfer_state);")
db.execSQL("CREATE INDEX IF NOT EXISTS attachment_remote_digest_index ON attachment (remote_digest);")
stopwatch.split("create-indexes")
db.execSQL(
"""
CREATE TRIGGER msl_attachment_delete AFTER DELETE ON attachment
BEGIN
DELETE FROM msl_payload WHERE _id IN (SELECT payload_id FROM msl_message WHERE msl_message.message_id = old.message_id);
END
"""
)
stopwatch.split("create-triggers")
stopwatch.stop(TAG)
}
}

View File

@@ -132,8 +132,6 @@ class ArchiveThumbnailUploadJob private constructor(
return Result.retry(defaultBackoff())
}
val mediaSecrets = mediaRootBackupKey.deriveMediaSecrets(attachment.requireThumbnailMediaName())
return when (val result = BackupRepository.copyThumbnailToArchive(attachmentPointer, attachment)) {
is NetworkResult.Success -> {
// save attachment thumbnail

View File

@@ -241,7 +241,7 @@ class RestoreAttachmentJob private constructor(
val downloadResult = if (useArchiveCdn) {
archiveFile = SignalDatabase.attachments.getOrCreateArchiveTransferFile(attachmentId)
val cdnCredentials = BackupRepository.getCdnReadCredentials(BackupRepository.CredentialType.MEDIA, attachment.archiveCdn).successOrThrow().headers
val cdnCredentials = BackupRepository.getCdnReadCredentials(BackupRepository.CredentialType.MEDIA, attachment.archiveCdn ?: RemoteConfig.backupFallbackArchiveCdn).successOrThrow().headers
messageReceiver
.retrieveArchivedAttachment(

View File

@@ -126,7 +126,7 @@ class RestoreAttachmentThumbnailJob private constructor(
override fun shouldCancel(): Boolean = this@RestoreAttachmentThumbnailJob.isCanceled
}
val cdnCredentials = BackupRepository.getCdnReadCredentials(BackupRepository.CredentialType.MEDIA, attachment.archiveCdn).successOrThrow().headers
val cdnCredentials = BackupRepository.getCdnReadCredentials(BackupRepository.CredentialType.MEDIA, attachment.archiveCdn ?: RemoteConfig.backupFallbackArchiveCdn).successOrThrow().headers
val pointer = attachment.createArchiveThumbnailPointer()
Log.i(TAG, "Downloading thumbnail for $attachmentId")
@@ -142,7 +142,7 @@ class RestoreAttachmentThumbnailJob private constructor(
progressListener
)
SignalDatabase.attachments.finalizeAttachmentThumbnailAfterDownload(attachmentId, attachment.remoteDigest!!, downloadResult.dataStream, thumbnailTransferFile)
SignalDatabase.attachments.finalizeAttachmentThumbnailAfterDownload(attachmentId, attachment.remoteDigest, downloadResult.dataStream, thumbnailTransferFile)
if (!SignalDatabase.messages.isStory(messageId)) {
AppDependencies.messageNotifier.updateNotification(context)

View File

@@ -982,6 +982,13 @@ object RemoteConfig {
BuildConfig.MESSAGE_BACKUP_RESTORE_ENABLED || value.asBoolean(false)
}
val backupFallbackArchiveCdn: Int by remoteInt(
key = "global.backups.mediaTierFallbackCdnNumber",
hotSwappable = true,
active = true,
defaultValue = 3
)
/** Whether unauthenticated chat web socket is backed by libsignal-net */
@JvmStatic
@get:JvmName("libSignalWebSocketEnabled")