Add initial thumbnail restore for message backup.
This commit is contained in:
parent
757c0fd2ea
commit
b72d586748
25 changed files with 397 additions and 50 deletions
|
@ -22,6 +22,9 @@ class ArchivedAttachment : Attachment {
|
|||
@JvmField
|
||||
val archiveMediaId: String
|
||||
|
||||
@JvmField
|
||||
val archiveThumbnailMediaId: String
|
||||
|
||||
constructor(
|
||||
contentType: String?,
|
||||
size: Long,
|
||||
|
@ -31,6 +34,7 @@ class ArchivedAttachment : Attachment {
|
|||
archiveCdn: Int?,
|
||||
archiveMediaName: String,
|
||||
archiveMediaId: String,
|
||||
archiveThumbnailMediaId: String,
|
||||
digest: ByteArray,
|
||||
incrementalMac: ByteArray?,
|
||||
incrementalMacChunkSize: Int?,
|
||||
|
@ -70,12 +74,14 @@ class ArchivedAttachment : Attachment {
|
|||
this.archiveCdn = archiveCdn ?: Cdn.CDN_3.cdnNumber
|
||||
this.archiveMediaName = archiveMediaName
|
||||
this.archiveMediaId = archiveMediaId
|
||||
this.archiveThumbnailMediaId = archiveThumbnailMediaId
|
||||
}
|
||||
|
||||
constructor(parcel: Parcel) : super(parcel) {
|
||||
archiveCdn = parcel.readInt()
|
||||
archiveMediaName = parcel.readString()!!
|
||||
archiveMediaId = parcel.readString()!!
|
||||
archiveThumbnailMediaId = parcel.readString()!!
|
||||
}
|
||||
|
||||
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||
|
@ -83,8 +89,10 @@ class ArchivedAttachment : Attachment {
|
|||
dest.writeInt(archiveCdn)
|
||||
dest.writeString(archiveMediaName)
|
||||
dest.writeString(archiveMediaId)
|
||||
dest.writeString(archiveThumbnailMediaId)
|
||||
}
|
||||
|
||||
override val uri: Uri? = null
|
||||
override val publicUri: Uri? = null
|
||||
override val thumbnailUri: Uri? = null
|
||||
}
|
||||
|
|
|
@ -70,6 +70,7 @@ abstract class Attachment(
|
|||
|
||||
abstract val uri: Uri?
|
||||
abstract val publicUri: Uri?
|
||||
abstract val thumbnailUri: Uri?
|
||||
|
||||
protected constructor(parcel: Parcel) : this(
|
||||
contentType = parcel.readString()!!,
|
||||
|
@ -129,7 +130,7 @@ abstract class Attachment(
|
|||
}
|
||||
|
||||
val isInProgress: Boolean
|
||||
get() = transferState != AttachmentTable.TRANSFER_PROGRESS_DONE && transferState != AttachmentTable.TRANSFER_PROGRESS_FAILED && transferState != AttachmentTable.TRANSFER_PROGRESS_PERMANENT_FAILURE
|
||||
get() = transferState != AttachmentTable.TRANSFER_PROGRESS_DONE && transferState != AttachmentTable.TRANSFER_PROGRESS_FAILED && transferState != AttachmentTable.TRANSFER_PROGRESS_PERMANENT_FAILURE && transferState != AttachmentTable.TRANSFER_RESTORE_OFFLOADED
|
||||
|
||||
val isPermanentlyFailed: Boolean
|
||||
get() = transferState == AttachmentTable.TRANSFER_PROGRESS_PERMANENT_FAILURE
|
||||
|
|
|
@ -28,12 +28,16 @@ class DatabaseAttachment : Attachment {
|
|||
@JvmField
|
||||
val archiveCdn: Int
|
||||
|
||||
@JvmField
|
||||
val archiveThumbnailCdn: Int
|
||||
|
||||
@JvmField
|
||||
val archiveMediaName: String?
|
||||
|
||||
@JvmField
|
||||
val archiveMediaId: String?
|
||||
|
||||
private val hasArchiveThumbnail: Boolean
|
||||
private val hasThumbnail: Boolean
|
||||
val displayOrder: Int
|
||||
|
||||
|
@ -42,6 +46,7 @@ class DatabaseAttachment : Attachment {
|
|||
mmsId: Long,
|
||||
hasData: Boolean,
|
||||
hasThumbnail: Boolean,
|
||||
hasArchiveThumbnail: Boolean,
|
||||
contentType: String?,
|
||||
transferProgress: Int,
|
||||
size: Long,
|
||||
|
@ -68,6 +73,7 @@ class DatabaseAttachment : Attachment {
|
|||
uploadTimestamp: Long,
|
||||
dataHash: String?,
|
||||
archiveCdn: Int,
|
||||
archiveThumbnailCdn: Int,
|
||||
archiveMediaName: String?,
|
||||
archiveMediaId: String?
|
||||
) : super(
|
||||
|
@ -99,8 +105,10 @@ class DatabaseAttachment : Attachment {
|
|||
this.hasData = hasData
|
||||
this.dataHash = dataHash
|
||||
this.hasThumbnail = hasThumbnail
|
||||
this.hasArchiveThumbnail = hasArchiveThumbnail
|
||||
this.displayOrder = displayOrder
|
||||
this.archiveCdn = archiveCdn
|
||||
this.archiveThumbnailCdn = archiveThumbnailCdn
|
||||
this.archiveMediaName = archiveMediaName
|
||||
this.archiveMediaId = archiveMediaId
|
||||
}
|
||||
|
@ -113,8 +121,10 @@ class DatabaseAttachment : Attachment {
|
|||
mmsId = parcel.readLong()
|
||||
displayOrder = parcel.readInt()
|
||||
archiveCdn = parcel.readInt()
|
||||
archiveThumbnailCdn = parcel.readInt()
|
||||
archiveMediaName = parcel.readString()
|
||||
archiveMediaId = parcel.readString()
|
||||
hasArchiveThumbnail = ParcelUtil.readBoolean(parcel)
|
||||
}
|
||||
|
||||
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||
|
@ -126,8 +136,10 @@ class DatabaseAttachment : Attachment {
|
|||
dest.writeLong(mmsId)
|
||||
dest.writeInt(displayOrder)
|
||||
dest.writeInt(archiveCdn)
|
||||
dest.writeInt(archiveThumbnailCdn)
|
||||
dest.writeString(archiveMediaName)
|
||||
dest.writeString(archiveMediaId)
|
||||
ParcelUtil.writeBoolean(dest, hasArchiveThumbnail)
|
||||
}
|
||||
|
||||
override val uri: Uri?
|
||||
|
@ -144,6 +156,13 @@ class DatabaseAttachment : Attachment {
|
|||
null
|
||||
}
|
||||
|
||||
override val thumbnailUri: Uri?
|
||||
get() = if (hasArchiveThumbnail) {
|
||||
PartAuthority.getAttachmentThumbnailUri(attachmentId)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
return other != null &&
|
||||
other is DatabaseAttachment && other.attachmentId == attachmentId
|
||||
|
|
|
@ -66,6 +66,7 @@ class PointerAttachment : Attachment {
|
|||
|
||||
override val uri: Uri? = null
|
||||
override val publicUri: Uri? = null
|
||||
override val thumbnailUri: Uri? = null
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
|
|
|
@ -80,4 +80,5 @@ class TombstoneAttachment : Attachment {
|
|||
|
||||
override val uri: Uri? = null
|
||||
override val publicUri: Uri? = null
|
||||
override val thumbnailUri: Uri? = null
|
||||
}
|
||||
|
|
|
@ -98,6 +98,7 @@ class UriAttachment : Attachment {
|
|||
|
||||
override val uri: Uri
|
||||
override val publicUri: Uri? = null
|
||||
override val thumbnailUri: Uri? = null
|
||||
|
||||
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||
super.writeToParcel(dest, flags)
|
||||
|
|
|
@ -18,6 +18,7 @@ import org.thoughtcrime.securesms.attachments.Attachment
|
|||
import org.thoughtcrime.securesms.attachments.AttachmentId
|
||||
import org.thoughtcrime.securesms.attachments.Cdn
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository.getThumbnailMediaName
|
||||
import org.thoughtcrime.securesms.backup.v2.database.ChatItemImportInserter
|
||||
import org.thoughtcrime.securesms.backup.v2.database.clearAllDataForBackupRestore
|
||||
import org.thoughtcrime.securesms.backup.v2.processor.AccountDataProcessor
|
||||
|
@ -346,7 +347,7 @@ object BackupRepository {
|
|||
/**
|
||||
* Retrieves an upload spec that can be used to upload attachment media.
|
||||
*/
|
||||
fun getMediaUploadSpec(): NetworkResult<ResumableUploadSpec> {
|
||||
fun getMediaUploadSpec(secretKey: ByteArray? = null): NetworkResult<ResumableUploadSpec> {
|
||||
val api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi
|
||||
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
|
||||
|
||||
|
@ -355,20 +356,21 @@ object BackupRepository {
|
|||
api.getMediaUploadForm(backupKey, credential)
|
||||
}
|
||||
.then { form ->
|
||||
api.getResumableUploadSpec(form)
|
||||
api.getResumableUploadSpec(form, secretKey)
|
||||
}
|
||||
}
|
||||
|
||||
fun archiveThumbnail(thumbnailAttachment: Attachment, parentAttachment: DatabaseAttachment): NetworkResult<ArchiveMediaResponse> {
|
||||
val api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi
|
||||
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
|
||||
val request = thumbnailAttachment.toArchiveMediaRequest(parentAttachment.getThumbnailMediaName(), backupKey)
|
||||
|
||||
return initBackupAndFetchAuth(backupKey)
|
||||
.then { credential ->
|
||||
api.archiveAttachmentMedia(
|
||||
backupKey = backupKey,
|
||||
serviceCredential = credential,
|
||||
item = thumbnailAttachment.toArchiveMediaRequest(parentAttachment.getThumbnailMediaName(), backupKey)
|
||||
item = request
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -390,7 +392,8 @@ object BackupRepository {
|
|||
.map { Triple(mediaName, request.mediaId, it) }
|
||||
}
|
||||
.map { (mediaName, mediaId, response) ->
|
||||
SignalDatabase.attachments.setArchiveData(attachmentId = attachment.attachmentId, archiveCdn = response.cdn, archiveMediaName = mediaName.name, archiveMediaId = mediaId)
|
||||
val thumbnailId = backupKey.deriveMediaId(attachment.getThumbnailMediaName()).encode()
|
||||
SignalDatabase.attachments.setArchiveData(attachmentId = attachment.attachmentId, archiveCdn = response.cdn, archiveMediaName = mediaName.name, archiveMediaId = mediaId, archiveThumbnailMediaId = thumbnailId)
|
||||
}
|
||||
.also { Log.i(TAG, "archiveMediaResult: $it") }
|
||||
}
|
||||
|
@ -427,7 +430,8 @@ object BackupRepository {
|
|||
.forEach {
|
||||
val attachmentId = result.mediaIdToAttachmentId(it.mediaId)
|
||||
val mediaName = result.attachmentIdToMediaName(attachmentId)
|
||||
SignalDatabase.attachments.setArchiveData(attachmentId = attachmentId, archiveCdn = it.cdn!!, archiveMediaName = mediaName, archiveMediaId = it.mediaId)
|
||||
val thumbnailId = backupKey.deriveMediaId(MediaName.forThumbnailFromMediaName(mediaName = mediaName)).encode()
|
||||
SignalDatabase.attachments.setArchiveData(attachmentId = attachmentId, archiveCdn = it.cdn!!, archiveMediaName = mediaName, archiveMediaId = it.mediaId, thumbnailId)
|
||||
}
|
||||
result
|
||||
}
|
||||
|
|
|
@ -658,6 +658,7 @@ class ChatItemImportInserter(
|
|||
archiveCdn = pointer.backupLocator.cdnNumber,
|
||||
archiveMediaName = pointer.backupLocator.mediaName,
|
||||
archiveMediaId = backupState.backupKey.deriveMediaId(MediaName(pointer.backupLocator.mediaName)).encode(),
|
||||
archiveThumbnailMediaId = backupState.backupKey.deriveMediaId(MediaName.forThumbnailFromMediaName(pointer.backupLocator.mediaName)).encode(),
|
||||
digest = pointer.backupLocator.digest.toByteArray(),
|
||||
incrementalMac = pointer.incrementalMac?.toByteArray(),
|
||||
incrementalMacChunkSize = pointer.incrementalMacChunkSize,
|
||||
|
|
|
@ -125,6 +125,8 @@ class AttachmentTable(
|
|||
const val DATA_RANDOM = "data_random"
|
||||
const val DATA_HASH_START = "data_hash_start"
|
||||
const val DATA_HASH_END = "data_hash_end"
|
||||
const val THUMBNAIL_FILE = "thumbnail_file"
|
||||
const val THUMBNAIL_RANDOM = "thumbnail_random"
|
||||
const val FILE_NAME = "file_name"
|
||||
const val FAST_PREFLIGHT_ID = "fast_preflight_id"
|
||||
const val VOICE_NOTE = "voice_note"
|
||||
|
@ -145,6 +147,8 @@ class AttachmentTable(
|
|||
const val ARCHIVE_CDN = "archive_cdn"
|
||||
const val ARCHIVE_MEDIA_NAME = "archive_media_name"
|
||||
const val ARCHIVE_MEDIA_ID = "archive_media_id"
|
||||
const val ARCHIVE_THUMBNAIL_MEDIA_ID = "archive_thumbnail_media_id"
|
||||
const val ARCHIVE_THUMBNAIL_CDN = "archive_thumbnail_cdn"
|
||||
const val ARCHIVE_TRANSFER_FILE = "archive_transfer_file"
|
||||
const val ARCHIVE_TRANSFER_STATE = "archive_transfer_state"
|
||||
|
||||
|
@ -159,6 +163,7 @@ class AttachmentTable(
|
|||
const val TRANSFER_PROGRESS_PERMANENT_FAILURE = 4
|
||||
const val TRANSFER_NEEDS_RESTORE = 5
|
||||
const val TRANSFER_RESTORE_IN_PROGRESS = 6
|
||||
const val TRANSFER_RESTORE_OFFLOADED = 7
|
||||
const val PREUPLOAD_MESSAGE_ID: Long = -8675309
|
||||
|
||||
private val PROJECTION = arrayOf(
|
||||
|
@ -196,9 +201,11 @@ class AttachmentTable(
|
|||
DATA_HASH_START,
|
||||
DATA_HASH_END,
|
||||
ARCHIVE_CDN,
|
||||
ARCHIVE_THUMBNAIL_CDN,
|
||||
ARCHIVE_MEDIA_NAME,
|
||||
ARCHIVE_MEDIA_ID,
|
||||
ARCHIVE_TRANSFER_FILE
|
||||
ARCHIVE_TRANSFER_FILE,
|
||||
THUMBNAIL_FILE
|
||||
)
|
||||
|
||||
@JvmField
|
||||
|
@ -240,8 +247,12 @@ class AttachmentTable(
|
|||
$ARCHIVE_CDN INTEGER DEFAULT 0,
|
||||
$ARCHIVE_MEDIA_NAME TEXT DEFAULT NULL,
|
||||
$ARCHIVE_MEDIA_ID TEXT DEFAULT NULL,
|
||||
$ARCHIVE_THUMBNAIL_MEDIA_ID TEXT DEFAULT NULL,
|
||||
$ARCHIVE_THUMBNAIL_CDN INTEGER DEFAULT 0,
|
||||
$ARCHIVE_TRANSFER_FILE TEXT DEFAULT NULL,
|
||||
$ARCHIVE_TRANSFER_STATE INTEGER DEFAULT ${ArchiveTransferState.NONE.value}
|
||||
$ARCHIVE_TRANSFER_STATE INTEGER DEFAULT ${ArchiveTransferState.NONE.value},
|
||||
$THUMBNAIL_FILE TEXT DEFAULT NULL,
|
||||
$THUMBNAIL_RANDOM BLOB DEFAULT NULL
|
||||
)
|
||||
"""
|
||||
|
||||
|
@ -273,6 +284,15 @@ class AttachmentTable(
|
|||
} ?: throw IOException("No stream for: $attachmentId")
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun getAttachmentThumbnailStream(attachmentId: AttachmentId, offset: Long): InputStream {
|
||||
return try {
|
||||
getThumbnailStream(attachmentId, offset)
|
||||
} catch (e: FileNotFoundException) {
|
||||
throw IOException("No stream for: $attachmentId", e)
|
||||
} ?: throw IOException("No stream for: $attachmentId")
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a [File] for an attachment that has no [DATA_HASH_END] and is in the [TRANSFER_PROGRESS_DONE] state, if present.
|
||||
*/
|
||||
|
@ -826,6 +846,36 @@ class AttachmentTable(
|
|||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun finalizeAttachmentThumbnailAfterDownload(attachmentId: AttachmentId, archiveMediaId: String, inputStream: InputStream, transferFile: File) {
|
||||
Log.i(TAG, "[finalizeAttachmentThumbnailAfterDownload] Finalizing downloaded data for $attachmentId.")
|
||||
val fileWriteResult: DataFileWriteResult = writeToDataFile(newDataFile(context), inputStream, TransformProperties.empty())
|
||||
|
||||
writableDatabase.withinTransaction { db ->
|
||||
val values = contentValuesOf(
|
||||
THUMBNAIL_FILE to fileWriteResult.file.absolutePath,
|
||||
THUMBNAIL_RANDOM to fileWriteResult.random
|
||||
)
|
||||
|
||||
db.update(TABLE_NAME)
|
||||
.values(values)
|
||||
.where("$ARCHIVE_MEDIA_ID = ?", archiveMediaId)
|
||||
.run()
|
||||
|
||||
db.update(TABLE_NAME)
|
||||
.values(TRANSFER_STATE to TRANSFER_RESTORE_OFFLOADED)
|
||||
.where("$ID = ?", attachmentId.id)
|
||||
.run()
|
||||
}
|
||||
|
||||
notifyConversationListListeners()
|
||||
notifyAttachmentListeners()
|
||||
|
||||
if (!transferFile.delete()) {
|
||||
Log.w(TAG, "Unable to delete transfer file.")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Needs to be called after an attachment is successfully uploaded. Writes metadata around it's final remote location, as well as calculates
|
||||
* it's ending hash, which is critical for backups.
|
||||
|
@ -1158,6 +1208,10 @@ class AttachmentTable(
|
|||
return transferFile
|
||||
}
|
||||
|
||||
fun createArchiveThumbnailTransferFile(): File {
|
||||
return newTransferFile()
|
||||
}
|
||||
|
||||
fun getDataFileInfo(attachmentId: AttachmentId): DataFileInfo? {
|
||||
return readableDatabase
|
||||
.select(ID, DATA_FILE, DATA_SIZE, DATA_RANDOM, DATA_HASH_START, DATA_HASH_END, TRANSFORM_PROPERTIES, UPLOAD_TIMESTAMP)
|
||||
|
@ -1173,6 +1227,21 @@ class AttachmentTable(
|
|||
}
|
||||
}
|
||||
|
||||
fun getThumbnailFileInfo(attachmentId: AttachmentId): ThumbnailFileInfo? {
|
||||
return readableDatabase
|
||||
.select(ID, THUMBNAIL_FILE, THUMBNAIL_RANDOM)
|
||||
.from(TABLE_NAME)
|
||||
.where("$ID = ?", attachmentId.id)
|
||||
.run()
|
||||
.readToSingleObject { cursor ->
|
||||
if (cursor.isNull(THUMBNAIL_FILE)) {
|
||||
null
|
||||
} else {
|
||||
cursor.readThumbnailFileInfo()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getDataFilePath(attachmentId: AttachmentId): String? {
|
||||
return readableDatabase
|
||||
.select(DATA_FILE)
|
||||
|
@ -1320,8 +1389,10 @@ class AttachmentTable(
|
|||
uploadTimestamp = jsonObject.getLong(UPLOAD_TIMESTAMP),
|
||||
dataHash = jsonObject.getString(DATA_HASH_END),
|
||||
archiveCdn = jsonObject.getInt(ARCHIVE_CDN),
|
||||
archiveThumbnailCdn = jsonObject.getInt(ARCHIVE_THUMBNAIL_CDN),
|
||||
archiveMediaName = jsonObject.getString(ARCHIVE_MEDIA_NAME),
|
||||
archiveMediaId = jsonObject.getString(ARCHIVE_MEDIA_ID)
|
||||
archiveMediaId = jsonObject.getString(ARCHIVE_MEDIA_ID),
|
||||
hasArchiveThumbnail = !TextUtils.isEmpty(jsonObject.getString(THUMBNAIL_FILE))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1361,13 +1432,14 @@ class AttachmentTable(
|
|||
return readableDatabase.rawQuery(query, null)
|
||||
}
|
||||
|
||||
fun setArchiveData(attachmentId: AttachmentId, archiveCdn: Int, archiveMediaName: String, archiveMediaId: String) {
|
||||
fun setArchiveData(attachmentId: AttachmentId, archiveCdn: Int, archiveMediaName: String, archiveMediaId: String, archiveThumbnailMediaId: String) {
|
||||
writableDatabase
|
||||
.update(TABLE_NAME)
|
||||
.values(
|
||||
ARCHIVE_CDN to archiveCdn,
|
||||
ARCHIVE_MEDIA_ID to archiveMediaId,
|
||||
ARCHIVE_MEDIA_NAME to archiveMediaName,
|
||||
ARCHIVE_THUMBNAIL_MEDIA_ID to archiveThumbnailMediaId,
|
||||
ARCHIVE_TRANSFER_STATE to ArchiveTransferState.FINISHED.value
|
||||
)
|
||||
.where("$ID = ?", attachmentId.id)
|
||||
|
@ -1375,13 +1447,14 @@ class AttachmentTable(
|
|||
}
|
||||
|
||||
fun updateArchiveCdnByMediaId(archiveMediaId: String, archiveCdn: Int): Int {
|
||||
return writableDatabase
|
||||
.update(TABLE_NAME)
|
||||
.values(
|
||||
ARCHIVE_CDN to archiveCdn
|
||||
)
|
||||
.where("$ARCHIVE_MEDIA_ID = ?", archiveMediaId)
|
||||
.run()
|
||||
return writableDatabase.rawQuery(
|
||||
"UPDATE $TABLE_NAME SET " +
|
||||
"$ARCHIVE_THUMBNAIL_CDN = CASE WHEN $ARCHIVE_THUMBNAIL_MEDIA_ID = ? THEN ? ELSE $ARCHIVE_THUMBNAIL_CDN END," +
|
||||
"$ARCHIVE_CDN = CASE WHEN $ARCHIVE_MEDIA_ID = ? THEN ? ELSE $ARCHIVE_CDN END " +
|
||||
"WHERE $ARCHIVE_MEDIA_ID = ? OR $ARCHIVE_THUMBNAIL_MEDIA_ID = ? " +
|
||||
"RETURNING $ARCHIVE_CDN, $ARCHIVE_THUMBNAIL_CDN",
|
||||
SqlUtil.buildArgs(archiveMediaId, archiveCdn, archiveMediaId, archiveCdn, archiveMediaId, archiveMediaId)
|
||||
).count
|
||||
}
|
||||
|
||||
fun clearArchiveData(attachmentIds: List<AttachmentId>) {
|
||||
|
@ -1485,6 +1558,21 @@ class AttachmentTable(
|
|||
}
|
||||
}
|
||||
|
||||
@Throws(FileNotFoundException::class)
|
||||
private fun getThumbnailStream(attachmentId: AttachmentId, offset: Long): InputStream? {
|
||||
val thumbnailInfo = getThumbnailFileInfo(attachmentId) ?: return null
|
||||
|
||||
return try {
|
||||
ModernDecryptingPartInputStream.createFor(attachmentSecret, thumbnailInfo.random, thumbnailInfo.file, offset)
|
||||
} catch (e: FileNotFoundException) {
|
||||
Log.w(TAG, e)
|
||||
throw e
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun newTransferFile(): File {
|
||||
val partsDirectory = context.getDir(DIRECTORY, Context.MODE_PRIVATE)
|
||||
|
@ -1664,6 +1752,7 @@ class AttachmentTable(
|
|||
put(ARCHIVE_CDN, attachment.archiveCdn)
|
||||
put(ARCHIVE_MEDIA_NAME, attachment.archiveMediaName)
|
||||
put(ARCHIVE_MEDIA_ID, attachment.archiveMediaId)
|
||||
put(ARCHIVE_THUMBNAIL_MEDIA_ID, attachment.archiveThumbnailMediaId)
|
||||
|
||||
attachment.stickerLocator?.let { sticker ->
|
||||
put(STICKER_PACK_ID, sticker.packId)
|
||||
|
@ -1874,8 +1963,10 @@ class AttachmentTable(
|
|||
uploadTimestamp = cursor.requireLong(UPLOAD_TIMESTAMP),
|
||||
dataHash = cursor.requireString(DATA_HASH_END),
|
||||
archiveCdn = cursor.requireInt(ARCHIVE_CDN),
|
||||
archiveThumbnailCdn = cursor.requireInt(ARCHIVE_THUMBNAIL_CDN),
|
||||
archiveMediaName = cursor.requireString(ARCHIVE_MEDIA_NAME),
|
||||
archiveMediaId = cursor.requireString(ARCHIVE_MEDIA_ID)
|
||||
archiveMediaId = cursor.requireString(ARCHIVE_MEDIA_ID),
|
||||
hasArchiveThumbnail = !cursor.isNull(THUMBNAIL_FILE)
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1900,6 +1991,14 @@ class AttachmentTable(
|
|||
)
|
||||
}
|
||||
|
||||
private fun Cursor.readThumbnailFileInfo(): ThumbnailFileInfo {
|
||||
return ThumbnailFileInfo(
|
||||
id = AttachmentId(this.requireLong(ID)),
|
||||
file = File(this.requireNonNullString(THUMBNAIL_FILE)),
|
||||
random = this.requireNonNullBlob(THUMBNAIL_RANDOM)
|
||||
)
|
||||
}
|
||||
|
||||
private fun Cursor.readStickerLocator(): StickerLocator? {
|
||||
return if (this.requireInt(STICKER_ID) >= 0) {
|
||||
StickerLocator(
|
||||
|
@ -1954,6 +2053,13 @@ class AttachmentTable(
|
|||
val uploadTimestamp: Long
|
||||
)
|
||||
|
||||
@VisibleForTesting
|
||||
class ThumbnailFileInfo(
|
||||
val id: AttachmentId,
|
||||
val file: File,
|
||||
val random: ByteArray
|
||||
)
|
||||
|
||||
@Parcelize
|
||||
data class TransformProperties(
|
||||
@JsonProperty("skipTransform")
|
||||
|
|
|
@ -357,7 +357,8 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
|||
'${AttachmentTable.MESSAGE_ID}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.MESSAGE_ID},
|
||||
'${AttachmentTable.DATA_SIZE}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.DATA_SIZE},
|
||||
'${AttachmentTable.FILE_NAME}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.FILE_NAME},
|
||||
'${AttachmentTable.DATA_FILE}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.DATA_FILE},
|
||||
'${AttachmentTable.DATA_FILE}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.DATA_FILE},
|
||||
'${AttachmentTable.THUMBNAIL_FILE}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.THUMBNAIL_FILE},
|
||||
'${AttachmentTable.CONTENT_TYPE}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.CONTENT_TYPE},
|
||||
'${AttachmentTable.CDN_NUMBER}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.CDN_NUMBER},
|
||||
'${AttachmentTable.REMOTE_LOCATION}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.REMOTE_LOCATION},
|
||||
|
@ -381,6 +382,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
|||
'${AttachmentTable.UPLOAD_TIMESTAMP}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.UPLOAD_TIMESTAMP},
|
||||
'${AttachmentTable.DATA_HASH_END}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.DATA_HASH_END},
|
||||
'${AttachmentTable.ARCHIVE_CDN}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.ARCHIVE_CDN},
|
||||
'${AttachmentTable.ARCHIVE_THUMBNAIL_CDN}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.ARCHIVE_THUMBNAIL_CDN},
|
||||
'${AttachmentTable.ARCHIVE_MEDIA_NAME}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.ARCHIVE_MEDIA_NAME},
|
||||
'${AttachmentTable.ARCHIVE_MEDIA_ID}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.ARCHIVE_MEDIA_ID}
|
||||
)
|
||||
|
|
|
@ -88,6 +88,7 @@ import org.thoughtcrime.securesms.database.helpers.migration.V227_AddAttachmentA
|
|||
import org.thoughtcrime.securesms.database.helpers.migration.V228_AddNameCollisionTables
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V229_MarkMissedCallEventsNotified
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V230_UnreadCountIndices
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V231_ArchiveThumbnailColumns
|
||||
|
||||
/**
|
||||
* Contains all of the database migrations for [SignalDatabase]. Broken into a separate file for cleanliness.
|
||||
|
@ -178,10 +179,11 @@ object SignalDatabaseMigrations {
|
|||
227 to V227_AddAttachmentArchiveTransferState,
|
||||
228 to V228_AddNameCollisionTables,
|
||||
229 to V229_MarkMissedCallEventsNotified,
|
||||
230 to V230_UnreadCountIndices
|
||||
230 to V230_UnreadCountIndices,
|
||||
231 to V231_ArchiveThumbnailColumns
|
||||
)
|
||||
|
||||
const val DATABASE_VERSION = 230
|
||||
const val DATABASE_VERSION = 231
|
||||
|
||||
@JvmStatic
|
||||
fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* 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
|
||||
|
||||
object V231_ArchiveThumbnailColumns : SignalDatabaseMigration {
|
||||
override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
db.execSQL("ALTER TABLE attachment ADD COLUMN thumbnail_file TEXT DEFAULT NULL")
|
||||
db.execSQL("ALTER TABLE attachment ADD COLUMN thumbnail_random BLOB DEFAULT NULL")
|
||||
db.execSQL("ALTER TABLE attachment ADD COLUMN archive_thumbnail_cdn INTEGER DEFAULT 0")
|
||||
db.execSQL("ALTER TABLE attachment ADD COLUMN archive_thumbnail_media_id TEXT DEFAULT NULL")
|
||||
}
|
||||
}
|
|
@ -13,6 +13,7 @@ import org.thoughtcrime.securesms.attachments.AttachmentId
|
|||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
|
||||
import org.thoughtcrime.securesms.attachments.PointerAttachment
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository.getThumbnailMediaName
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.jobmanager.Job
|
||||
|
@ -86,8 +87,9 @@ class ArchiveThumbnailUploadJob private constructor(
|
|||
Log.w(TAG, "Unable to generate a thumbnail result for $attachmentId")
|
||||
return Result.success()
|
||||
}
|
||||
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
|
||||
|
||||
val resumableUpload = when (val result = BackupRepository.getMediaUploadSpec()) {
|
||||
val resumableUpload = when (val result = BackupRepository.getMediaUploadSpec(secretKey = backupKey.deriveThumbnailTransitKey(attachment.getThumbnailMediaName()))) {
|
||||
is NetworkResult.Success -> {
|
||||
Log.d(TAG, "Got an upload spec!")
|
||||
result.result.toProto()
|
||||
|
@ -116,9 +118,13 @@ class ArchiveThumbnailUploadJob private constructor(
|
|||
return Result.retry(defaultBackoff())
|
||||
}
|
||||
|
||||
val backupDirectories = BackupRepository.getCdnBackupDirectories().successOrThrow()
|
||||
val mediaSecrets = backupKey.deriveMediaSecrets(attachment.getThumbnailMediaName())
|
||||
|
||||
return when (val result = BackupRepository.archiveThumbnail(attachmentPointer, attachment)) {
|
||||
is NetworkResult.Success -> {
|
||||
Log.d(TAG, "Successfully archived thumbnail for $attachmentId")
|
||||
Log.i(RestoreAttachmentJob.TAG, "Restore: Thumbnail mediaId=${mediaSecrets.id.encode()} backupDir=${backupDirectories.backupDir} mediaDir=${backupDirectories.mediaDir}")
|
||||
Log.d(TAG, "Successfully archived thumbnail for $attachmentId mediaName=${attachment.getThumbnailMediaName()}")
|
||||
Result.success()
|
||||
}
|
||||
is NetworkResult.NetworkError -> {
|
||||
|
|
|
@ -72,11 +72,15 @@ class BackupMessagesJob private constructor(parameters: Parameters) : BaseJob(pa
|
|||
when (val archiveResult = BackupRepository.archiveMedia(attachments)) {
|
||||
is NetworkResult.Success -> {
|
||||
Log.i(TAG, "Archive call successful")
|
||||
for (success in archiveResult.result.sourceNotFoundResponses) {
|
||||
val attachmentId = archiveResult.result.mediaIdToAttachmentId(success.mediaId)
|
||||
for (notFound in archiveResult.result.sourceNotFoundResponses) {
|
||||
val attachmentId = archiveResult.result.mediaIdToAttachmentId(notFound.mediaId)
|
||||
Log.i(TAG, "Attachment $attachmentId not found on cdn, will need to re-upload")
|
||||
needToBackfill++
|
||||
}
|
||||
for (success in archiveResult.result.successfulResponses) {
|
||||
val attachmentId = archiveResult.result.mediaIdToAttachmentId(success.mediaId)
|
||||
ArchiveThumbnailUploadJob.enqueueIfNecessary(attachmentId)
|
||||
}
|
||||
progress += attachments.size
|
||||
}
|
||||
|
||||
|
|
|
@ -62,7 +62,11 @@ class BackupRestoreMediaJob private constructor(parameters: Parameters) : BaseJo
|
|||
attachmentId = attachment.attachmentId,
|
||||
manual = false,
|
||||
forceArchiveDownload = true,
|
||||
fullSize = shouldRestoreFullSize(message, restoreTime, optimizeStorage = SignalStore.backup().optimizeStorage)
|
||||
restoreMode = if (shouldRestoreFullSize(message, restoreTime, optimizeStorage = SignalStore.backup().optimizeStorage)) {
|
||||
RestoreAttachmentJob.RestoreMode.ORIGINAL
|
||||
} else {
|
||||
RestoreAttachmentJob.RestoreMode.THUMBNAIL
|
||||
}
|
||||
)
|
||||
}
|
||||
jobManager.addAll(restoreJobBatch)
|
||||
|
@ -70,7 +74,7 @@ class BackupRestoreMediaJob private constructor(parameters: Parameters) : BaseJo
|
|||
}
|
||||
|
||||
private fun shouldRestoreFullSize(message: MmsMessageRecord, restoreTime: Long, optimizeStorage: Boolean): Boolean {
|
||||
return ((restoreTime - message.dateSent) < 30.days.inWholeMilliseconds) || !optimizeStorage
|
||||
return !optimizeStorage || ((restoreTime - message.dateSent) < 30.days.inWholeMilliseconds)
|
||||
}
|
||||
|
||||
override fun onShouldRetry(e: Exception): Boolean = false
|
||||
|
|
|
@ -15,6 +15,7 @@ import org.signal.libsignal.protocol.InvalidMessageException
|
|||
import org.thoughtcrime.securesms.attachments.AttachmentId
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository.getThumbnailMediaName
|
||||
import org.thoughtcrime.securesms.database.AttachmentTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
|
@ -52,18 +53,18 @@ class RestoreAttachmentJob private constructor(
|
|||
attachmentId: AttachmentId,
|
||||
private val manual: Boolean,
|
||||
private var forceArchiveDownload: Boolean,
|
||||
private val fullSize: Boolean
|
||||
private val restoreMode: RestoreMode
|
||||
) : BaseJob(parameters) {
|
||||
|
||||
companion object {
|
||||
const val KEY = "RestoreAttachmentJob"
|
||||
private val TAG = Log.tag(AttachmentDownloadJob::class.java)
|
||||
val TAG = Log.tag(AttachmentDownloadJob::class.java)
|
||||
|
||||
private const val KEY_MESSAGE_ID = "message_id"
|
||||
private const val KEY_ATTACHMENT_ID = "part_row_id"
|
||||
private const val KEY_MANUAL = "part_manual"
|
||||
private const val KEY_FORCE_ARCHIVE = "force_archive"
|
||||
private const val KEY_FULL_SIZE = "full_size"
|
||||
private const val KEY_RESTORE_MODE = "restore_mode"
|
||||
|
||||
@JvmStatic
|
||||
fun constructQueueString(attachmentId: AttachmentId): String {
|
||||
|
@ -71,13 +72,19 @@ class RestoreAttachmentJob private constructor(
|
|||
return "RestoreAttachmentJob"
|
||||
}
|
||||
|
||||
fun jobSpecMatchesAnyAttachmentId(jobSpec: JobSpec, ids: Set<AttachmentId>): Boolean {
|
||||
private fun getJsonJobData(jobSpec: JobSpec): JsonJobData? {
|
||||
if (KEY != jobSpec.factoryKey) {
|
||||
return false
|
||||
return null
|
||||
}
|
||||
|
||||
val serializedData = jobSpec.serializedData ?: return false
|
||||
val data = JsonJobData.deserialize(serializedData)
|
||||
val serializedData = jobSpec.serializedData ?: return null
|
||||
return JsonJobData.deserialize(serializedData)
|
||||
}
|
||||
|
||||
fun jobSpecMatchesAnyAttachmentId(data: JsonJobData?, ids: Set<AttachmentId>): Boolean {
|
||||
if (data == null) {
|
||||
return false
|
||||
}
|
||||
val parsed = AttachmentId(data.getLong(KEY_ATTACHMENT_ID))
|
||||
return ids.contains(parsed)
|
||||
}
|
||||
|
@ -85,8 +92,15 @@ class RestoreAttachmentJob private constructor(
|
|||
fun modifyPriorities(ids: Set<AttachmentId>, priority: Int) {
|
||||
val jobManager = ApplicationDependencies.getJobManager()
|
||||
jobManager.update { spec ->
|
||||
if (jobSpecMatchesAnyAttachmentId(spec, ids) && spec.priority != priority) {
|
||||
spec.copy(priority = priority)
|
||||
val jobData = getJsonJobData(spec)
|
||||
if (jobSpecMatchesAnyAttachmentId(jobData, ids) && spec.priority != priority) {
|
||||
val restoreMode = RestoreMode.deserialize(jobData!!.getIntOrDefault(KEY_RESTORE_MODE, RestoreMode.ORIGINAL.value))
|
||||
val modifiedJobData = if (restoreMode == RestoreMode.ORIGINAL) {
|
||||
jobData.buildUpon().putInt(KEY_RESTORE_MODE, RestoreMode.BOTH.value).build()
|
||||
} else {
|
||||
jobData
|
||||
}
|
||||
spec.copy(priority = priority, serializedData = modifiedJobData.serialize())
|
||||
} else {
|
||||
spec
|
||||
}
|
||||
|
@ -96,7 +110,7 @@ class RestoreAttachmentJob private constructor(
|
|||
|
||||
private val attachmentId: Long
|
||||
|
||||
constructor(messageId: Long, attachmentId: AttachmentId, manual: Boolean, forceArchiveDownload: Boolean = false, fullSize: Boolean = true) : this(
|
||||
constructor(messageId: Long, attachmentId: AttachmentId, manual: Boolean, forceArchiveDownload: Boolean = false, restoreMode: RestoreMode = RestoreMode.ORIGINAL) : this(
|
||||
Parameters.Builder()
|
||||
.setQueue(constructQueueString(attachmentId))
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
|
@ -107,7 +121,7 @@ class RestoreAttachmentJob private constructor(
|
|||
attachmentId,
|
||||
manual,
|
||||
forceArchiveDownload,
|
||||
fullSize
|
||||
restoreMode
|
||||
)
|
||||
|
||||
init {
|
||||
|
@ -120,7 +134,7 @@ class RestoreAttachmentJob private constructor(
|
|||
.putLong(KEY_ATTACHMENT_ID, attachmentId)
|
||||
.putBoolean(KEY_MANUAL, manual)
|
||||
.putBoolean(KEY_FORCE_ARCHIVE, forceArchiveDownload)
|
||||
.putBoolean(KEY_FULL_SIZE, fullSize)
|
||||
.putInt(KEY_RESTORE_MODE, restoreMode.value)
|
||||
.serialize()
|
||||
}
|
||||
|
||||
|
@ -166,12 +180,19 @@ class RestoreAttachmentJob private constructor(
|
|||
return
|
||||
}
|
||||
|
||||
if (attachment.transferState != AttachmentTable.TRANSFER_NEEDS_RESTORE && attachment.transferState != AttachmentTable.TRANSFER_RESTORE_IN_PROGRESS) {
|
||||
if (attachment.transferState != AttachmentTable.TRANSFER_NEEDS_RESTORE &&
|
||||
attachment.transferState != AttachmentTable.TRANSFER_RESTORE_IN_PROGRESS &&
|
||||
(attachment.transferState != AttachmentTable.TRANSFER_RESTORE_OFFLOADED || restoreMode == RestoreMode.THUMBNAIL)
|
||||
) {
|
||||
Log.w(TAG, "Attachment does not need to be restored.")
|
||||
return
|
||||
}
|
||||
|
||||
retrieveAttachment(messageId, attachmentId, attachment)
|
||||
if (attachment.thumbnailUri == null && (restoreMode == RestoreMode.THUMBNAIL || restoreMode == RestoreMode.BOTH)) {
|
||||
downloadThumbnail(attachmentId, attachment)
|
||||
}
|
||||
if (restoreMode == RestoreMode.ORIGINAL || restoreMode == RestoreMode.BOTH) {
|
||||
retrieveAttachment(messageId, attachmentId, attachment)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure() {
|
||||
|
@ -360,6 +381,102 @@ class RestoreAttachmentJob private constructor(
|
|||
}
|
||||
}
|
||||
|
||||
@Throws(InvalidPartException::class)
|
||||
private fun createThumbnailPointer(attachment: DatabaseAttachment): SignalServiceAttachmentPointer {
|
||||
if (TextUtils.isEmpty(attachment.remoteKey)) {
|
||||
throw InvalidPartException("empty encrypted key")
|
||||
}
|
||||
|
||||
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
|
||||
val backupDirectories = BackupRepository.getCdnBackupDirectories().successOrThrow()
|
||||
return try {
|
||||
val key = backupKey.deriveThumbnailTransitKey(attachment.getThumbnailMediaName())
|
||||
|
||||
if (attachment.remoteDigest != null) {
|
||||
Log.i(TAG, "Downloading attachment with digest: " + Hex.toString(attachment.remoteDigest))
|
||||
} else {
|
||||
Log.i(TAG, "Downloading attachment with no digest...")
|
||||
}
|
||||
|
||||
val mediaId = backupKey.deriveMediaId(attachment.getThumbnailMediaName()).encode()
|
||||
Log.i(TAG, "Restore: Thumbnail mediaId=$mediaId backupDir=${backupDirectories.backupDir} mediaDir=${backupDirectories.mediaDir}")
|
||||
|
||||
SignalServiceAttachmentPointer(
|
||||
attachment.archiveThumbnailCdn,
|
||||
SignalServiceAttachmentRemoteId.Backup(
|
||||
backupDir = backupDirectories.backupDir,
|
||||
mediaDir = backupDirectories.mediaDir,
|
||||
mediaId = mediaId
|
||||
),
|
||||
null,
|
||||
key,
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
0,
|
||||
0,
|
||||
Optional.ofNullable(attachment.remoteDigest),
|
||||
Optional.empty(),
|
||||
attachment.incrementalMacChunkSize,
|
||||
Optional.empty(),
|
||||
attachment.voiceNote,
|
||||
attachment.borderless,
|
||||
attachment.videoGif,
|
||||
Optional.empty(),
|
||||
Optional.ofNullable(attachment.blurHash).map { it.hash },
|
||||
attachment.uploadTimestamp
|
||||
)
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, e)
|
||||
throw InvalidPartException(e)
|
||||
} catch (e: ArithmeticException) {
|
||||
Log.w(TAG, e)
|
||||
throw InvalidPartException(e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun downloadThumbnail(attachmentId: AttachmentId, attachment: DatabaseAttachment) {
|
||||
if (attachment.transferState == AttachmentTable.TRANSFER_RESTORE_OFFLOADED) {
|
||||
Log.w(TAG, "$attachmentId already has thumbnail downloaded")
|
||||
return
|
||||
}
|
||||
if (attachment.archiveMediaName == null) {
|
||||
Log.w(TAG, "$attachmentId was never archived! Cannot proceed.")
|
||||
return
|
||||
}
|
||||
|
||||
val maxThumbnailSize: Long = FeatureFlags.maxAttachmentReceiveSizeBytes()
|
||||
val thumbnailTransferFile: File = SignalDatabase.attachments.createArchiveThumbnailTransferFile()
|
||||
val thumbnailFile: File = SignalDatabase.attachments.createArchiveThumbnailTransferFile()
|
||||
|
||||
val progressListener = object : SignalServiceAttachment.ProgressListener {
|
||||
override fun onAttachmentProgress(total: Long, progress: Long) {
|
||||
EventBus.getDefault().postSticky(PartProgressEvent(attachment, PartProgressEvent.Type.NETWORK, total, progress))
|
||||
}
|
||||
|
||||
override fun shouldCancel(): Boolean {
|
||||
return this@RestoreAttachmentJob.isCanceled
|
||||
}
|
||||
}
|
||||
|
||||
val cdnCredentials = BackupRepository.getCdnReadCredentials(attachment.archiveCdn).successOrThrow().headers
|
||||
val messageReceiver = ApplicationDependencies.getSignalServiceMessageReceiver()
|
||||
val pointer = createThumbnailPointer(attachment)
|
||||
|
||||
Log.w(TAG, "Downloading thumbnail for $attachmentId mediaName=${attachment.getThumbnailMediaName()}")
|
||||
val stream = messageReceiver
|
||||
.retrieveArchivedAttachment(
|
||||
SignalStore.svr().getOrCreateMasterKey().deriveBackupKey().deriveMediaSecrets(attachment.getThumbnailMediaName()),
|
||||
cdnCredentials,
|
||||
thumbnailTransferFile,
|
||||
pointer,
|
||||
thumbnailFile,
|
||||
maxThumbnailSize,
|
||||
progressListener
|
||||
)
|
||||
|
||||
SignalDatabase.attachments.finalizeAttachmentThumbnailAfterDownload(attachmentId, attachment.archiveMediaId!!, stream, thumbnailTransferFile)
|
||||
}
|
||||
|
||||
private fun markFailed(messageId: Long, attachmentId: AttachmentId) {
|
||||
try {
|
||||
SignalDatabase.attachments.setTransferProgressFailed(attachmentId, messageId)
|
||||
|
@ -382,6 +499,18 @@ class RestoreAttachmentJob private constructor(
|
|||
constructor(e: Exception?) : super(e)
|
||||
}
|
||||
|
||||
enum class RestoreMode(val value: Int) {
|
||||
THUMBNAIL(0),
|
||||
ORIGINAL(1),
|
||||
BOTH(2);
|
||||
|
||||
companion object {
|
||||
fun deserialize(value: Int): RestoreMode {
|
||||
return values().firstOrNull { it.value == value } ?: ORIGINAL
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class RemoteData(val remoteId: SignalServiceAttachmentRemoteId, val cdnNumber: Int)
|
||||
|
||||
class Factory : Job.Factory<RestoreAttachmentJob?> {
|
||||
|
@ -393,7 +522,7 @@ class RestoreAttachmentJob private constructor(
|
|||
attachmentId = AttachmentId(data.getLong(KEY_ATTACHMENT_ID)),
|
||||
manual = data.getBoolean(KEY_MANUAL),
|
||||
forceArchiveDownload = data.getBooleanOrDefault(KEY_FORCE_ARCHIVE, false),
|
||||
fullSize = data.getBooleanOrDefault(KEY_FULL_SIZE, true)
|
||||
restoreMode = RestoreMode.deserialize(data.getIntOrDefault(KEY_RESTORE_MODE, RestoreMode.ORIGINAL.value))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,11 +32,13 @@ public class PartAuthority {
|
|||
|
||||
private static final String AUTHORITY = BuildConfig.APPLICATION_ID;
|
||||
private static final String PART_URI_STRING = "content://" + AUTHORITY + "/part";
|
||||
private static final String PART_THUMBNAIL_STRING = "content://" + AUTHORITY + "/thumbnail";
|
||||
private static final String STICKER_URI_STRING = "content://" + AUTHORITY + "/sticker";
|
||||
private static final String WALLPAPER_URI_STRING = "content://" + AUTHORITY + "/wallpaper";
|
||||
private static final String EMOJI_URI_STRING = "content://" + AUTHORITY + "/emoji";
|
||||
private static final String AVATAR_PICKER_URI_STRING = "content://" + AUTHORITY + "/avatar_picker";
|
||||
private static final Uri PART_CONTENT_URI = Uri.parse(PART_URI_STRING);
|
||||
private static final Uri PART_THUMBNAIL_URI = Uri.parse(PART_THUMBNAIL_STRING);
|
||||
private static final Uri STICKER_CONTENT_URI = Uri.parse(STICKER_URI_STRING);
|
||||
private static final Uri WALLPAPER_CONTENT_URI = Uri.parse(WALLPAPER_URI_STRING);
|
||||
private static final Uri EMOJI_CONTENT_URI = Uri.parse(EMOJI_URI_STRING);
|
||||
|
@ -49,12 +51,14 @@ public class PartAuthority {
|
|||
private static final int WALLPAPER_ROW = 5;
|
||||
private static final int EMOJI_ROW = 6;
|
||||
private static final int AVATAR_PICKER_ROW = 7;
|
||||
private static final int THUMBNAIL_ROW = 8;
|
||||
|
||||
private static final UriMatcher uriMatcher;
|
||||
|
||||
static {
|
||||
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
|
||||
uriMatcher.addURI(AUTHORITY, "part/#", PART_ROW);
|
||||
uriMatcher.addURI(AUTHORITY, "thumbnail/#", THUMBNAIL_ROW);
|
||||
uriMatcher.addURI(AUTHORITY, "sticker/#", STICKER_ROW);
|
||||
uriMatcher.addURI(AUTHORITY, "wallpaper/*", WALLPAPER_ROW);
|
||||
uriMatcher.addURI(AUTHORITY, "emoji/*", EMOJI_ROW);
|
||||
|
@ -83,6 +87,7 @@ public class PartAuthority {
|
|||
case WALLPAPER_ROW: return WallpaperStorage.read(context, getWallpaperFilename(uri));
|
||||
case EMOJI_ROW: return EmojiFiles.openForReading(context, getEmojiFilename(uri));
|
||||
case AVATAR_PICKER_ROW: return AvatarPickerStorage.read(context, getAvatarPickerFilename(uri));
|
||||
case THUMBNAIL_ROW: return SignalDatabase.attachments().getAttachmentThumbnailStream(new PartUriParser(uri).getPartId(), 0);
|
||||
default: return openExternalFileStream(context, uri);
|
||||
}
|
||||
} catch (SecurityException se) {
|
||||
|
@ -178,7 +183,7 @@ public class PartAuthority {
|
|||
}
|
||||
|
||||
public static Uri getAttachmentThumbnailUri(AttachmentId attachmentId) {
|
||||
return getAttachmentDataUri(attachmentId);
|
||||
return ContentUris.withAppendedId(PART_THUMBNAIL_URI, attachmentId.id);
|
||||
}
|
||||
|
||||
public static Uri getStickerUri(long id) {
|
||||
|
|
|
@ -49,9 +49,18 @@ public abstract class Slide {
|
|||
return attachment.contentType;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Uri getThumbnailUri() {
|
||||
return attachment.getThumbnailUri();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Uri getUri() {
|
||||
return attachment.getUri();
|
||||
Uri attachmentUri = attachment.getUri();
|
||||
if (attachmentUri != null) {
|
||||
return attachmentUri;
|
||||
}
|
||||
return attachment.getThumbnailUri();
|
||||
}
|
||||
|
||||
public @Nullable Uri getPublicUri() {
|
||||
|
|
|
@ -61,4 +61,4 @@ message ArchiveAttachmentBackfillJobData {
|
|||
|
||||
message ArchiveThumbnailUploadJobData {
|
||||
uint64 attachmentId = 1;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -232,6 +232,7 @@ class UploadDependencyGraphTest {
|
|||
mmsId = AttachmentTable.PREUPLOAD_MESSAGE_ID,
|
||||
hasData = false,
|
||||
hasThumbnail = false,
|
||||
hasArchiveThumbnail = false,
|
||||
contentType = attachment.contentType,
|
||||
transferProgress = AttachmentTable.TRANSFER_PROGRESS_PENDING,
|
||||
size = attachment.size,
|
||||
|
@ -259,7 +260,8 @@ class UploadDependencyGraphTest {
|
|||
dataHash = null,
|
||||
archiveMediaId = null,
|
||||
archiveMediaName = null,
|
||||
archiveCdn = 0
|
||||
archiveCdn = 0,
|
||||
archiveThumbnailCdn = 0
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@ object FakeMessageRecords {
|
|||
mmsId: Long = 1,
|
||||
hasData: Boolean = true,
|
||||
hasThumbnail: Boolean = true,
|
||||
hasArchiveThumbnail: Boolean = false,
|
||||
contentType: String = MediaUtil.IMAGE_JPEG,
|
||||
transferProgress: Int = AttachmentTable.TRANSFER_PROGRESS_DONE,
|
||||
size: Long = 0L,
|
||||
|
@ -59,14 +60,17 @@ object FakeMessageRecords {
|
|||
uploadTimestamp: Long = 200,
|
||||
dataHash: String? = null,
|
||||
archiveCdn: Int = 0,
|
||||
archiveThumbnailCdn: Int = 0,
|
||||
archiveMediaName: String? = null,
|
||||
archiveMediaId: String? = null
|
||||
archiveMediaId: String? = null,
|
||||
archiveThumbnailId: String? = null
|
||||
): DatabaseAttachment {
|
||||
return DatabaseAttachment(
|
||||
attachmentId,
|
||||
mmsId,
|
||||
hasData,
|
||||
hasThumbnail,
|
||||
hasArchiveThumbnail,
|
||||
contentType,
|
||||
transferProgress,
|
||||
size,
|
||||
|
@ -93,6 +97,7 @@ object FakeMessageRecords {
|
|||
uploadTimestamp,
|
||||
dataHash,
|
||||
archiveCdn,
|
||||
archiveThumbnailCdn,
|
||||
archiveMediaId,
|
||||
archiveMediaName
|
||||
)
|
||||
|
|
|
@ -156,9 +156,13 @@ class ArchiveApi(
|
|||
}
|
||||
}
|
||||
|
||||
fun getResumableUploadSpec(uploadForm: AttachmentUploadForm): NetworkResult<ResumableUploadSpec> {
|
||||
fun getResumableUploadSpec(uploadForm: AttachmentUploadForm, secretKey: ByteArray?): NetworkResult<ResumableUploadSpec> {
|
||||
return NetworkResult.fromFetch {
|
||||
pushServiceSocket.getResumableUploadSpec(uploadForm)
|
||||
if (secretKey == null) {
|
||||
pushServiceSocket.getResumableUploadSpec(uploadForm)
|
||||
} else {
|
||||
pushServiceSocket.getResumableUploadSpecWithKey(uploadForm, secretKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -42,6 +42,10 @@ class BackupKey(val value: ByteArray) {
|
|||
return deriveMediaSecrets(deriveMediaId(mediaName))
|
||||
}
|
||||
|
||||
fun deriveThumbnailTransitKey(thumbnailMediaName: MediaName): ByteArray {
|
||||
return HKDF.deriveSecrets(value, deriveMediaId(thumbnailMediaName).value, "20240513_Signal_Backups_EncryptThumbnail".toByteArray(), 64)
|
||||
}
|
||||
|
||||
private fun deriveMediaSecrets(mediaId: MediaId): MediaKeyMaterial {
|
||||
val extendedKey = HKDF.deriveSecrets(this.value, mediaId.value, "20231003_Signal_Backups_EncryptMedia".toByteArray(), 80)
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ value class MediaName(val name: String) {
|
|||
companion object {
|
||||
fun fromDigest(digest: ByteArray) = MediaName(Base64.encodeWithoutPadding(digest))
|
||||
fun fromDigestForThumbnail(digest: ByteArray) = MediaName("${Base64.encodeWithoutPadding(digest)}_thumbnail")
|
||||
fun forThumbnailFromMediaName(mediaName: String) = MediaName("${mediaName}_thumbnail")
|
||||
}
|
||||
|
||||
fun toByteArray(): ByteArray {
|
||||
|
|
|
@ -1586,6 +1586,16 @@ public class PushServiceSocket {
|
|||
uploadForm.headers);
|
||||
}
|
||||
|
||||
public ResumableUploadSpec getResumableUploadSpecWithKey(AttachmentUploadForm uploadForm, byte[] secretKey) throws IOException {
|
||||
return new ResumableUploadSpec(secretKey,
|
||||
Util.getSecretBytes(16),
|
||||
uploadForm.key,
|
||||
uploadForm.cdn,
|
||||
getResumableUploadUrl(uploadForm),
|
||||
System.currentTimeMillis() + CDN2_RESUMABLE_LINK_LIFETIME_MILLIS,
|
||||
uploadForm.headers);
|
||||
}
|
||||
|
||||
public AttachmentDigest uploadAttachment(PushAttachmentData attachment) throws IOException {
|
||||
|
||||
if (attachment.getResumableUploadSpec() == null || attachment.getResumableUploadSpec().getExpirationTimestamp() < System.currentTimeMillis()) {
|
||||
|
|
Loading…
Add table
Reference in a new issue