mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-22 02:36:55 +00:00
Add ui wiring for archive thumbnail support.
This commit is contained in:
@@ -71,6 +71,8 @@ abstract class Attachment(
|
||||
abstract val uri: Uri?
|
||||
abstract val publicUri: Uri?
|
||||
abstract val thumbnailUri: Uri?
|
||||
val displayUri: Uri?
|
||||
get() = uri ?: thumbnailUri
|
||||
|
||||
protected constructor(parcel: Parcel) : this(
|
||||
contentType = parcel.readString()!!,
|
||||
|
||||
@@ -172,7 +172,7 @@ class DatabaseAttachment : Attachment {
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
return other != null &&
|
||||
other is DatabaseAttachment && other.attachmentId == attachmentId
|
||||
other is DatabaseAttachment && other.attachmentId == attachmentId && other.uri == uri
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
|
||||
@@ -11,7 +11,8 @@ import org.thoughtcrime.securesms.attachments.DatabaseAttachment
|
||||
import org.thoughtcrime.securesms.database.AttachmentTable
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
import org.thoughtcrime.securesms.jobs.RestoreAttachmentJob
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.jobs.RestoreAttachmentThumbnailJob
|
||||
|
||||
/**
|
||||
* Responsible for managing logic around restore prioritization
|
||||
@@ -28,17 +29,24 @@ object BackupRestoreManager {
|
||||
fun prioritizeAttachmentsIfNeeded(messageRecords: List<MessageRecord>) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
synchronized(this) {
|
||||
val restoringAttachments: List<AttachmentId> = messageRecords
|
||||
val restoringAttachments = messageRecords
|
||||
.mapNotNull { (it as? MmsMessageRecord?)?.slideDeck?.slides }
|
||||
.flatten()
|
||||
.mapNotNull { it.asAttachment() as? DatabaseAttachment }
|
||||
.filter { it.transferState == AttachmentTable.TRANSFER_RESTORE_IN_PROGRESS && !reprioritizedAttachments.contains(it.attachmentId) }
|
||||
.map { it.attachmentId }
|
||||
.filter {
|
||||
val needThumbnail = it.thumbnailRestoreState == AttachmentTable.ThumbnailRestoreState.NEEDS_RESTORE && it.transferState == AttachmentTable.TRANSFER_RESTORE_IN_PROGRESS
|
||||
(needThumbnail || it.thumbnailRestoreState == AttachmentTable.ThumbnailRestoreState.IN_PROGRESS) && !reprioritizedAttachments.contains(it.attachmentId)
|
||||
}
|
||||
.map { it.attachmentId to it.mmsId }
|
||||
.toSet()
|
||||
|
||||
reprioritizedAttachments += restoringAttachments
|
||||
|
||||
if (restoringAttachments.isNotEmpty()) {
|
||||
RestoreAttachmentJob.modifyPriorities(restoringAttachments.toSet(), 1)
|
||||
reprioritizedAttachments += restoringAttachments.map { it.first }
|
||||
val thumbnailJobs = restoringAttachments.map {
|
||||
val (attachmentId, mmsId) = it
|
||||
RestoreAttachmentThumbnailJob(attachmentId = attachmentId, messageId = mmsId, highPriority = true)
|
||||
}
|
||||
if (thumbnailJobs.isNotEmpty()) {
|
||||
AppDependencies.jobManager.addAll(thumbnailJobs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -370,7 +370,16 @@ public class ThumbnailView extends FrameLayout {
|
||||
transferControlViewStub.get().setSlides(List.of(slide));
|
||||
}
|
||||
int transferState = TransferControlView.getTransferState(List.of(slide));
|
||||
transferControlViewStub.get().setVisible(showControls && transferState != AttachmentTable.TRANSFER_PROGRESS_DONE && transferState != AttachmentTable.TRANSFER_PROGRESS_PERMANENT_FAILURE);
|
||||
boolean isOffloadedImage = transferState == AttachmentTable.TRANSFER_RESTORE_OFFLOADED && MediaUtil.isImageType(slide.getContentType());
|
||||
|
||||
if (!showControls ||
|
||||
transferState == AttachmentTable.TRANSFER_PROGRESS_DONE ||
|
||||
transferState == AttachmentTable.TRANSFER_PROGRESS_PERMANENT_FAILURE ||
|
||||
isOffloadedImage) {
|
||||
transferControlViewStub.get().setVisible(false);
|
||||
} else {
|
||||
transferControlViewStub.get().setVisible(true);
|
||||
}
|
||||
|
||||
if (slide.getUri() != null && slide.hasPlayOverlay() &&
|
||||
(slide.getTransferState() == AttachmentTable.TRANSFER_PROGRESS_DONE || isPreview))
|
||||
@@ -412,7 +421,10 @@ public class ThumbnailView extends FrameLayout {
|
||||
SettableFuture<Boolean> result = new SettableFuture<>();
|
||||
boolean resultHandled = false;
|
||||
|
||||
if (slide.hasPlaceholder() && (previousBlurHash == null || !Objects.equals(slide.getPlaceholderBlur(), previousBlurHash))) {
|
||||
if (slide.hasThumbnail()) {
|
||||
buildArchiveThumbnailRequestBuilder(requestManager, slide).into(new GlideBitmapListeningTarget(blurHash, result));
|
||||
resultHandled = true;
|
||||
} else if (slide.hasPlaceholder() && (previousBlurHash == null || !Objects.equals(slide.getPlaceholderBlur(), previousBlurHash))) {
|
||||
buildPlaceholderRequestBuilder(requestManager, slide).into(new GlideBitmapListeningTarget(blurHash, result));
|
||||
resultHandled = true;
|
||||
} else if (!slide.hasPlaceholder()) {
|
||||
@@ -420,7 +432,7 @@ public class ThumbnailView extends FrameLayout {
|
||||
blurHash.setImageDrawable(null);
|
||||
}
|
||||
|
||||
if (slide.getUri() != null) {
|
||||
if (slide.getDisplayUri() != null) {
|
||||
if (!MediaUtil.isJpegType(slide.getContentType()) && !MediaUtil.isVideoType(slide.getContentType())) {
|
||||
SettableFuture<Boolean> thumbnailFuture = new SettableFuture<>();
|
||||
thumbnailFuture.deferTo(result);
|
||||
@@ -542,7 +554,7 @@ public class ThumbnailView extends FrameLayout {
|
||||
}
|
||||
|
||||
private RequestBuilder<Drawable> buildThumbnailRequestBuilder(@NonNull RequestManager requestManager, @NonNull Slide slide) {
|
||||
RequestBuilder<Drawable> requestBuilder = applySizing(requestManager.load(new DecryptableUri(Objects.requireNonNull(slide.getUri())))
|
||||
RequestBuilder<Drawable> requestBuilder = applySizing(requestManager.load(new DecryptableUri(Objects.requireNonNull(slide.getDisplayUri())))
|
||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
||||
.downsample(SignalDownsampleStrategy.CENTER_OUTSIDE_NO_UPSCALE)
|
||||
.transition(withCrossFade()));
|
||||
@@ -611,6 +623,25 @@ public class ThumbnailView extends FrameLayout {
|
||||
}
|
||||
}
|
||||
|
||||
private RequestBuilder<Bitmap> buildArchiveThumbnailRequestBuilder(@NonNull RequestManager requestManager, @NonNull Slide slide) {
|
||||
RequestBuilder<Bitmap> bitmap = requestManager.asBitmap();
|
||||
|
||||
Uri thumbnailUri = slide.getThumbnailUri();
|
||||
|
||||
if (thumbnailUri != null) {
|
||||
bitmap = bitmap.load(slide.getThumbnailUri());
|
||||
} else {
|
||||
bitmap = bitmap.load(slide.getPlaceholderRes(getContext().getTheme()));
|
||||
}
|
||||
|
||||
final RequestBuilder<Bitmap> resizedRequest = applySizing(bitmap.diskCacheStrategy(DiskCacheStrategy.NONE));
|
||||
if (thumbnailUri != null) {
|
||||
return resizedRequest.centerCrop();
|
||||
} else {
|
||||
return resizedRequest;
|
||||
}
|
||||
}
|
||||
|
||||
private <TranscodeType> RequestBuilder<TranscodeType> applySizing(@NonNull RequestBuilder<TranscodeType> request) {
|
||||
int[] size = new int[2];
|
||||
fillTargetDimensions(size, dimens, bounds);
|
||||
@@ -648,13 +679,8 @@ public class ThumbnailView extends FrameLayout {
|
||||
private class ThumbnailClickDispatcher implements View.OnClickListener {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
boolean validThumbnail = slide != null &&
|
||||
slide.asAttachment().getUri() != null &&
|
||||
slide.getTransferState() == AttachmentTable.TRANSFER_PROGRESS_DONE;
|
||||
|
||||
boolean permanentFailure = slide != null && slide.asAttachment().isPermanentlyFailed();
|
||||
|
||||
if (thumbnailClickListener != null && (validThumbnail || permanentFailure)) {
|
||||
boolean controlsVisible = transferControlViewStub.getVisibility() == View.VISIBLE;
|
||||
if (thumbnailClickListener != null && !controlsVisible) {
|
||||
thumbnailClickListener.onClick(view, slide);
|
||||
} else if (parentClickListener != null) {
|
||||
parentClickListener.onClick(view);
|
||||
|
||||
@@ -2468,10 +2468,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
Log.i(TAG, "Scheduling push attachment downloads for " + slides.size() + " items");
|
||||
|
||||
for (Slide slide : slides) {
|
||||
AppDependencies.getJobManager().add(new AttachmentDownloadJob(messageRecord.getId(),
|
||||
((DatabaseAttachment) slide.asAttachment()).attachmentId,
|
||||
true,
|
||||
false));
|
||||
AttachmentDownloadJob.downloadAttachmentIfNeeded((DatabaseAttachment) slide.asAttachment());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2484,37 +2481,12 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
|
||||
@Override
|
||||
public void onClick(View v, Slide slide) {
|
||||
if (messageRecord.isOutgoing()) {
|
||||
Log.d(TAG, "Video player button for outgoing slide clicked.");
|
||||
return;
|
||||
}
|
||||
if (MediaUtil.isInstantVideoSupported(slide)) {
|
||||
final DatabaseAttachment databaseAttachment = (DatabaseAttachment) slide.asAttachment();
|
||||
if (databaseAttachment.transferState == AttachmentTable.TRANSFER_RESTORE_OFFLOADED) {
|
||||
final AttachmentId attachmentId = databaseAttachment.attachmentId;
|
||||
final JobManager jobManager = AppDependencies.getJobManager();
|
||||
final String queue = RestoreAttachmentJob.constructQueueString(attachmentId);
|
||||
String jobId = AttachmentDownloadJob.downloadAttachmentIfNeeded(databaseAttachment);
|
||||
if (jobId != null) {
|
||||
setup(v, slide);
|
||||
jobManager.add(new RestoreAttachmentJob(messageRecord.getId(),
|
||||
attachmentId,
|
||||
true,
|
||||
false,
|
||||
RestoreAttachmentJob.RestoreMode.ORIGINAL));
|
||||
jobManager.addListener(queue, (job, jobState) -> {
|
||||
if (jobState.isComplete()) {
|
||||
cleanup();
|
||||
}
|
||||
});
|
||||
} else if (databaseAttachment.transferState != AttachmentTable.TRANSFER_PROGRESS_STARTED) {
|
||||
final AttachmentId attachmentId = databaseAttachment.attachmentId;
|
||||
final JobManager jobManager = AppDependencies.getJobManager();
|
||||
final String queue = AttachmentDownloadJob.constructQueueString(attachmentId);
|
||||
setup(v, slide);
|
||||
jobManager.add(new AttachmentDownloadJob(messageRecord.getId(),
|
||||
attachmentId,
|
||||
true,
|
||||
false));
|
||||
jobManager.addListener(queue, (job, jobState) -> {
|
||||
AppDependencies.getJobManager().addListener(jobId, (job, jobState) -> {
|
||||
if (jobState.isComplete()) {
|
||||
cleanup();
|
||||
}
|
||||
@@ -2590,7 +2562,8 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
performClick();
|
||||
} else if (!canPlayContent && mediaItem != null && eventListener != null) {
|
||||
eventListener.onPlayInlineContent(conversationMessage);
|
||||
} else if (MediaPreviewV2Fragment.isContentTypeSupported(slide.getContentType()) && slide.getUri() != null) {
|
||||
} else if (MediaPreviewV2Fragment.isContentTypeSupported(slide.getContentType()) && slide.getDisplayUri() != null) {
|
||||
AttachmentDownloadJob.downloadAttachmentIfNeeded((DatabaseAttachment) slide.asAttachment());
|
||||
launchMediaPreview(v, slide);
|
||||
} else if (slide.getUri() != null) {
|
||||
Log.i(TAG, "Clicked: " + slide.getUri() + " , " + slide.getContentType());
|
||||
@@ -2634,8 +2607,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
return;
|
||||
}
|
||||
|
||||
Uri mediaUri = slide.getUri();
|
||||
|
||||
Uri mediaUri = slide.getDisplayUri();
|
||||
if (mediaUri == null) {
|
||||
Log.w(TAG, "Could not launch media preview for item: uri was null");
|
||||
return;
|
||||
|
||||
@@ -739,6 +739,17 @@ class AttachmentTable(
|
||||
notifyConversationListeners(threadId)
|
||||
}
|
||||
|
||||
fun setThumbnailTransferState(messageId: Long, attachmentId: AttachmentId, thumbnailRestoreState: ThumbnailRestoreState) {
|
||||
writableDatabase
|
||||
.update(TABLE_NAME)
|
||||
.values(THUMBNAIL_RESTORE_STATE to thumbnailRestoreState.value)
|
||||
.where("$ID = ?", attachmentId.id)
|
||||
.run()
|
||||
|
||||
val threadId = messages.getThreadIdForMessage(messageId)
|
||||
notifyConversationListeners(threadId)
|
||||
}
|
||||
|
||||
@Throws(MmsException::class)
|
||||
fun setTransferProgressFailed(attachmentId: AttachmentId, mmsId: Long) {
|
||||
writableDatabase
|
||||
@@ -750,6 +761,17 @@ class AttachmentTable(
|
||||
notifyConversationListeners(messages.getThreadIdForMessage(mmsId))
|
||||
}
|
||||
|
||||
@Throws(MmsException::class)
|
||||
fun setThumbnailRestoreProgressFailed(attachmentId: AttachmentId, mmsId: Long) {
|
||||
writableDatabase
|
||||
.update(TABLE_NAME)
|
||||
.values(THUMBNAIL_RESTORE_STATE to ThumbnailRestoreState.PERMANENT_FAILURE.value)
|
||||
.where("$ID = ? AND $THUMBNAIL_RESTORE_STATE != ?", attachmentId.id, ThumbnailRestoreState.FINISHED)
|
||||
.run()
|
||||
|
||||
notifyConversationListeners(messages.getThreadIdForMessage(mmsId))
|
||||
}
|
||||
|
||||
@Throws(MmsException::class)
|
||||
fun setTransferProgressPermanentFailure(attachmentId: AttachmentId, mmsId: Long) {
|
||||
writableDatabase
|
||||
@@ -860,18 +882,14 @@ class AttachmentTable(
|
||||
writableDatabase.withinTransaction { db ->
|
||||
val values = contentValuesOf(
|
||||
THUMBNAIL_FILE to fileWriteResult.file.absolutePath,
|
||||
THUMBNAIL_RANDOM to fileWriteResult.random
|
||||
THUMBNAIL_RANDOM to fileWriteResult.random,
|
||||
THUMBNAIL_RESTORE_STATE to ThumbnailRestoreState.FINISHED.value
|
||||
)
|
||||
|
||||
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()
|
||||
@@ -1776,6 +1794,7 @@ class AttachmentTable(
|
||||
put(ARCHIVE_MEDIA_NAME, attachment.archiveMediaName)
|
||||
put(ARCHIVE_MEDIA_ID, attachment.archiveMediaId)
|
||||
put(ARCHIVE_THUMBNAIL_MEDIA_ID, attachment.archiveThumbnailMediaId)
|
||||
put(THUMBNAIL_RESTORE_STATE, ThumbnailRestoreState.NEEDS_RESTORE.value)
|
||||
|
||||
attachment.stickerLocator?.let { sticker ->
|
||||
put(STICKER_PACK_ID, sticker.packId)
|
||||
|
||||
@@ -9,6 +9,7 @@ import org.signal.core.util.requireLong
|
||||
import org.signal.core.util.requireNonNullString
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.thoughtcrime.securesms.util.MediaUtil
|
||||
import org.thoughtcrime.securesms.util.MediaUtil.SlideType
|
||||
|
||||
@@ -115,6 +116,16 @@ class MediaTable internal constructor(context: Context?, databaseHelper: SignalD
|
||||
"""
|
||||
)
|
||||
|
||||
private val GALLERY_MEDIA_QUERY_INCLUDING_TEMP_VIDEOS_AND_THUMBNAILS = String.format(
|
||||
BASE_MEDIA_QUERY,
|
||||
"""
|
||||
(${AttachmentTable.DATA_FILE} IS NOT NULL OR (${AttachmentTable.CONTENT_TYPE} LIKE 'video/%' AND ${AttachmentTable.REMOTE_INCREMENTAL_DIGEST} IS NOT NULL) OR (${AttachmentTable.THUMBNAIL_FILE} IS NOT NULL)) AND
|
||||
${AttachmentTable.CONTENT_TYPE} NOT LIKE 'image/svg%' AND
|
||||
(${AttachmentTable.CONTENT_TYPE} LIKE 'image/%' OR ${AttachmentTable.CONTENT_TYPE} LIKE 'video/%') AND
|
||||
${MessageTable.LINK_PREVIEWS} IS NULL
|
||||
"""
|
||||
)
|
||||
|
||||
private val AUDIO_MEDIA_QUERY = String.format(
|
||||
BASE_MEDIA_QUERY,
|
||||
"""
|
||||
@@ -153,7 +164,11 @@ class MediaTable internal constructor(context: Context?, databaseHelper: SignalD
|
||||
|
||||
@JvmOverloads
|
||||
fun getGalleryMediaForThread(threadId: Long, sorting: Sorting, limit: Int = 0): Cursor {
|
||||
var query = sorting.applyToQuery(applyEqualityOperator(threadId, GALLERY_MEDIA_QUERY_INCLUDING_TEMP_VIDEOS))
|
||||
var query = if (FeatureFlags.messageBackups()) {
|
||||
sorting.applyToQuery(applyEqualityOperator(threadId, GALLERY_MEDIA_QUERY_INCLUDING_TEMP_VIDEOS_AND_THUMBNAILS))
|
||||
} else {
|
||||
sorting.applyToQuery(applyEqualityOperator(threadId, GALLERY_MEDIA_QUERY_INCLUDING_TEMP_VIDEOS))
|
||||
}
|
||||
val args = arrayOf(threadId.toString() + "")
|
||||
|
||||
if (limit > 0) {
|
||||
|
||||
@@ -84,6 +84,26 @@ class AttachmentDownloadJob private constructor(
|
||||
val parsed = AttachmentId(data.getLong(KEY_ATTACHMENT_ID))
|
||||
return attachmentId == parsed
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun downloadAttachmentIfNeeded(databaseAttachment: DatabaseAttachment): String? {
|
||||
if (databaseAttachment.transferState == AttachmentTable.TRANSFER_RESTORE_OFFLOADED) {
|
||||
return RestoreAttachmentJob.restoreOffloadedAttachment(databaseAttachment)
|
||||
} else if (databaseAttachment.transferState != AttachmentTable.TRANSFER_PROGRESS_STARTED &&
|
||||
databaseAttachment.transferState != AttachmentTable.TRANSFER_PROGRESS_DONE &&
|
||||
databaseAttachment.transferState != AttachmentTable.TRANSFER_PROGRESS_PERMANENT_FAILURE
|
||||
) {
|
||||
val downloadJob = AttachmentDownloadJob(
|
||||
messageId = databaseAttachment.mmsId,
|
||||
attachmentId = databaseAttachment.attachmentId,
|
||||
manual = true,
|
||||
forceArchiveDownload = false
|
||||
)
|
||||
AppDependencies.jobManager.add(downloadJob)
|
||||
return downloadJob.id
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private val attachmentId: Long
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
package org.thoughtcrime.securesms.jobs
|
||||
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.database.AttachmentTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
@@ -50,24 +51,33 @@ class BackupRestoreMediaJob private constructor(parameters: Parameters) : BaseJo
|
||||
val jobManager = AppDependencies.jobManager
|
||||
val batchSize = 100
|
||||
val restoreTime = System.currentTimeMillis()
|
||||
var restoreJobBatch: List<RestoreAttachmentJob>
|
||||
var restoreJobBatch: List<Job>
|
||||
do {
|
||||
val attachmentBatch = SignalDatabase.attachments.getRestorableAttachments(batchSize)
|
||||
val messageIds = attachmentBatch.map { it.mmsId }.toSet()
|
||||
val messageMap = SignalDatabase.messages.getMessages(messageIds).associate { it.id to (it as MmsMessageRecord) }
|
||||
restoreJobBatch = SignalDatabase.attachments.getRestorableAttachments(batchSize).map { attachment ->
|
||||
val message = messageMap[attachment.mmsId]!!
|
||||
RestoreAttachmentJob(
|
||||
messageId = attachment.mmsId,
|
||||
attachmentId = attachment.attachmentId,
|
||||
manual = false,
|
||||
forceArchiveDownload = true,
|
||||
restoreMode = if (shouldRestoreFullSize(message, restoreTime, optimizeStorage = SignalStore.backup().optimizeStorage)) {
|
||||
RestoreAttachmentJob.RestoreMode.ORIGINAL
|
||||
} else {
|
||||
RestoreAttachmentJob.RestoreMode.THUMBNAIL
|
||||
}
|
||||
)
|
||||
if (shouldRestoreFullSize(message, restoreTime, SignalStore.backup().optimizeStorage)) {
|
||||
RestoreAttachmentJob(
|
||||
messageId = attachment.mmsId,
|
||||
attachmentId = attachment.attachmentId,
|
||||
manual = false,
|
||||
forceArchiveDownload = true,
|
||||
restoreMode = RestoreAttachmentJob.RestoreMode.ORIGINAL
|
||||
)
|
||||
} else {
|
||||
SignalDatabase.attachments.setTransferState(
|
||||
messageId = attachment.mmsId,
|
||||
attachmentId = attachment.attachmentId,
|
||||
transferState = AttachmentTable.TRANSFER_RESTORE_OFFLOADED
|
||||
)
|
||||
RestoreAttachmentThumbnailJob(
|
||||
messageId = attachment.mmsId,
|
||||
attachmentId = attachment.attachmentId,
|
||||
highPriority = false
|
||||
)
|
||||
}
|
||||
}
|
||||
jobManager.addAll(restoreJobBatch)
|
||||
} while (restoreJobBatch.isNotEmpty())
|
||||
|
||||
@@ -211,6 +211,7 @@ public final class JobManagerFactories {
|
||||
put(RequestGroupV2InfoWorkerJob.KEY, new RequestGroupV2InfoWorkerJob.Factory());
|
||||
put(RequestGroupV2InfoJob.KEY, new RequestGroupV2InfoJob.Factory());
|
||||
put(RestoreAttachmentJob.KEY, new RestoreAttachmentJob.Factory());
|
||||
put(RestoreAttachmentThumbnailJob.KEY, new RestoreAttachmentThumbnailJob.Factory());
|
||||
put(RetrieveProfileAvatarJob.KEY, new RetrieveProfileAvatarJob.Factory());
|
||||
put(RetrieveProfileJob.KEY, new RetrieveProfileJob.Factory());
|
||||
put(RetrieveRemoteAnnouncementsJob.KEY, new RetrieveRemoteAnnouncementsJob.Factory());
|
||||
|
||||
@@ -89,22 +89,17 @@ class RestoreAttachmentJob private constructor(
|
||||
return ids.contains(parsed)
|
||||
}
|
||||
|
||||
fun modifyPriorities(ids: Set<AttachmentId>, priority: Int) {
|
||||
val jobManager = AppDependencies.jobManager
|
||||
jobManager.update { spec ->
|
||||
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
|
||||
}
|
||||
}
|
||||
@JvmStatic
|
||||
fun restoreOffloadedAttachment(attachment: DatabaseAttachment): String {
|
||||
val restoreJob = RestoreAttachmentJob(
|
||||
messageId = attachment.mmsId,
|
||||
attachmentId = attachment.attachmentId,
|
||||
manual = false,
|
||||
forceArchiveDownload = true,
|
||||
restoreMode = RestoreAttachmentJob.RestoreMode.ORIGINAL
|
||||
)
|
||||
AppDependencies.jobManager.add(restoreJob)
|
||||
return restoreJob.id
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,7 +143,7 @@ class RestoreAttachmentJob private constructor(
|
||||
val attachmentId = AttachmentId(attachmentId)
|
||||
val attachment = SignalDatabase.attachments.getAttachment(attachmentId)
|
||||
val pending = attachment != null && attachment.transferState != AttachmentTable.TRANSFER_PROGRESS_DONE && attachment.transferState != AttachmentTable.TRANSFER_PROGRESS_PERMANENT_FAILURE
|
||||
if (attachment?.transferState == AttachmentTable.TRANSFER_NEEDS_RESTORE) {
|
||||
if (attachment?.transferState == AttachmentTable.TRANSFER_NEEDS_RESTORE || attachment?.transferState == AttachmentTable.TRANSFER_RESTORE_OFFLOADED) {
|
||||
Log.i(TAG, "onAdded() Marking attachment restore progress as 'started'")
|
||||
SignalDatabase.attachments.setTransferState(messageId, attachmentId, AttachmentTable.TRANSFER_RESTORE_IN_PROGRESS)
|
||||
}
|
||||
@@ -427,10 +422,18 @@ class RestoreAttachmentJob private constructor(
|
||||
}
|
||||
|
||||
private fun downloadThumbnail(attachmentId: AttachmentId, attachment: DatabaseAttachment) {
|
||||
if (attachment.transferState == AttachmentTable.TRANSFER_RESTORE_OFFLOADED) {
|
||||
if (attachment.thumbnailRestoreState == AttachmentTable.ThumbnailRestoreState.FINISHED) {
|
||||
Log.w(TAG, "$attachmentId already has thumbnail downloaded")
|
||||
return
|
||||
}
|
||||
if (attachment.thumbnailRestoreState == AttachmentTable.ThumbnailRestoreState.NONE) {
|
||||
Log.w(TAG, "$attachmentId has no thumbnail state")
|
||||
return
|
||||
}
|
||||
if (attachment.thumbnailRestoreState == AttachmentTable.ThumbnailRestoreState.PERMANENT_FAILURE) {
|
||||
Log.w(TAG, "$attachmentId thumbnail permanently failed")
|
||||
return
|
||||
}
|
||||
if (attachment.archiveMediaName == null) {
|
||||
Log.w(TAG, "$attachmentId was never archived! Cannot proceed.")
|
||||
return
|
||||
@@ -442,7 +445,6 @@ class RestoreAttachmentJob private constructor(
|
||||
|
||||
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 {
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.thoughtcrime.securesms.jobs
|
||||
|
||||
import android.text.TextUtils
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import org.signal.core.util.logging.Log
|
||||
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.AppDependencies
|
||||
import org.thoughtcrime.securesms.jobmanager.Job
|
||||
import org.thoughtcrime.securesms.jobmanager.JobLogger.format
|
||||
import org.thoughtcrime.securesms.jobmanager.JsonJobData
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.mms.MmsException
|
||||
import org.thoughtcrime.securesms.notifications.v2.ConversationId.Companion.forConversation
|
||||
import org.thoughtcrime.securesms.transport.RetryLaterException
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId
|
||||
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.util.Optional
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* Download attachment from locations as specified in their record.
|
||||
*/
|
||||
class RestoreAttachmentThumbnailJob private constructor(
|
||||
parameters: Parameters,
|
||||
private val messageId: Long,
|
||||
attachmentId: AttachmentId
|
||||
) : BaseJob(parameters) {
|
||||
|
||||
companion object {
|
||||
const val KEY = "RestoreAttachmentThumbnailJob"
|
||||
val TAG = Log.tag(RestoreAttachmentThumbnailJob::class.java)
|
||||
|
||||
private const val KEY_MESSAGE_ID = "message_id"
|
||||
private const val KEY_ATTACHMENT_ID = "part_row_id"
|
||||
|
||||
@JvmStatic
|
||||
fun constructQueueString(attachmentId: AttachmentId): String {
|
||||
// TODO: decide how many queues
|
||||
return "RestoreAttachmentThumbnailJob"
|
||||
}
|
||||
}
|
||||
|
||||
private val attachmentId: Long
|
||||
|
||||
constructor(messageId: Long, attachmentId: AttachmentId, highPriority: Boolean = false) : this(
|
||||
Parameters.Builder()
|
||||
.setQueue(constructQueueString(attachmentId))
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setLifespan(TimeUnit.DAYS.toMillis(1))
|
||||
.setMaxAttempts(Parameters.UNLIMITED)
|
||||
.setPriority(if (highPriority) Parameters.PRIORITY_HIGH else Parameters.PRIORITY_DEFAULT)
|
||||
.build(),
|
||||
messageId,
|
||||
attachmentId
|
||||
)
|
||||
|
||||
init {
|
||||
this.attachmentId = attachmentId.id
|
||||
}
|
||||
|
||||
override fun serialize(): ByteArray? {
|
||||
return JsonJobData.Builder()
|
||||
.putLong(KEY_MESSAGE_ID, messageId)
|
||||
.putLong(KEY_ATTACHMENT_ID, attachmentId)
|
||||
.serialize()
|
||||
}
|
||||
|
||||
override fun getFactoryKey(): String {
|
||||
return KEY
|
||||
}
|
||||
|
||||
override fun onAdded() {
|
||||
Log.i(TAG, "onAdded() messageId: $messageId attachmentId: $attachmentId")
|
||||
|
||||
val attachmentId = AttachmentId(attachmentId)
|
||||
val attachment = SignalDatabase.attachments.getAttachment(attachmentId)
|
||||
val pending = attachment != null &&
|
||||
attachment.thumbnailRestoreState != AttachmentTable.ThumbnailRestoreState.FINISHED &&
|
||||
attachment.thumbnailRestoreState != AttachmentTable.ThumbnailRestoreState.PERMANENT_FAILURE &&
|
||||
attachment.thumbnailRestoreState != AttachmentTable.ThumbnailRestoreState.NONE
|
||||
if (pending) {
|
||||
Log.i(TAG, "onAdded() Marking thumbnail restore progress as 'started'")
|
||||
SignalDatabase.attachments.setThumbnailTransferState(messageId, attachmentId, AttachmentTable.ThumbnailRestoreState.IN_PROGRESS)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
public override fun onRun() {
|
||||
doWork()
|
||||
|
||||
if (!SignalDatabase.messages.isStory(messageId)) {
|
||||
AppDependencies.messageNotifier.updateNotification(context, forConversation(0))
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class, RetryLaterException::class)
|
||||
fun doWork() {
|
||||
Log.i(TAG, "onRun() messageId: $messageId attachmentId: $attachmentId")
|
||||
|
||||
val attachmentId = AttachmentId(attachmentId)
|
||||
val attachment = SignalDatabase.attachments.getAttachment(attachmentId)
|
||||
|
||||
if (attachment == null) {
|
||||
Log.w(TAG, "attachment no longer exists.")
|
||||
return
|
||||
}
|
||||
|
||||
downloadThumbnail(attachmentId, attachment)
|
||||
}
|
||||
|
||||
override fun onFailure() {
|
||||
Log.w(TAG, format(this, "onFailure() thumbnail messageId: $messageId attachmentId: $attachmentId "))
|
||||
|
||||
val attachmentId = AttachmentId(attachmentId)
|
||||
markFailed(messageId, attachmentId)
|
||||
}
|
||||
|
||||
override fun onShouldRetry(exception: Exception): Boolean {
|
||||
return exception is PushNetworkException ||
|
||||
exception is RetryLaterException
|
||||
}
|
||||
|
||||
@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())
|
||||
val mediaId = backupKey.deriveMediaId(attachment.getThumbnailMediaName()).encode()
|
||||
SignalServiceAttachmentPointer(
|
||||
attachment.archiveThumbnailCdn,
|
||||
SignalServiceAttachmentRemoteId.Backup(
|
||||
backupDir = backupDirectories.backupDir,
|
||||
mediaDir = backupDirectories.mediaDir,
|
||||
mediaId = mediaId
|
||||
),
|
||||
null,
|
||||
key,
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
0,
|
||||
0,
|
||||
Optional.empty(),
|
||||
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.thumbnailRestoreState == AttachmentTable.ThumbnailRestoreState.FINISHED) {
|
||||
Log.w(TAG, "$attachmentId already has thumbnail downloaded")
|
||||
return
|
||||
}
|
||||
if (attachment.thumbnailRestoreState == AttachmentTable.ThumbnailRestoreState.NONE) {
|
||||
Log.w(TAG, "$attachmentId has no thumbnail state")
|
||||
return
|
||||
}
|
||||
if (attachment.thumbnailRestoreState == AttachmentTable.ThumbnailRestoreState.PERMANENT_FAILURE) {
|
||||
Log.w(TAG, "$attachmentId thumbnail permanently failed")
|
||||
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) {
|
||||
}
|
||||
|
||||
override fun shouldCancel(): Boolean {
|
||||
return this@RestoreAttachmentThumbnailJob.isCanceled
|
||||
}
|
||||
}
|
||||
|
||||
val cdnCredentials = BackupRepository.getCdnReadCredentials(attachment.archiveCdn).successOrThrow().headers
|
||||
val messageReceiver = AppDependencies.signalServiceMessageReceiver
|
||||
val pointer = createThumbnailPointer(attachment)
|
||||
|
||||
Log.w(TAG, "Downloading thumbnail for $attachmentId")
|
||||
val stream = messageReceiver
|
||||
.retrieveArchivedAttachment(
|
||||
SignalStore.svr().getOrCreateMasterKey().deriveBackupKey().deriveMediaSecrets(attachment.getThumbnailMediaName()),
|
||||
cdnCredentials,
|
||||
thumbnailTransferFile,
|
||||
pointer,
|
||||
thumbnailFile,
|
||||
maxThumbnailSize,
|
||||
true,
|
||||
progressListener
|
||||
)
|
||||
|
||||
SignalDatabase.attachments.finalizeAttachmentThumbnailAfterDownload(attachmentId, attachment.archiveMediaId!!, stream, thumbnailTransferFile)
|
||||
}
|
||||
|
||||
private fun markFailed(messageId: Long, attachmentId: AttachmentId) {
|
||||
try {
|
||||
SignalDatabase.attachments.setThumbnailRestoreProgressFailed(attachmentId, messageId)
|
||||
} catch (e: MmsException) {
|
||||
Log.w(TAG, e)
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
internal class InvalidPartException : Exception {
|
||||
constructor(s: String?) : super(s)
|
||||
constructor(e: Exception?) : super(e)
|
||||
}
|
||||
|
||||
class Factory : Job.Factory<RestoreAttachmentThumbnailJob?> {
|
||||
override fun create(parameters: Parameters, serializedData: ByteArray?): RestoreAttachmentThumbnailJob {
|
||||
val data = JsonJobData.deserialize(serializedData)
|
||||
return RestoreAttachmentThumbnailJob(
|
||||
parameters = parameters,
|
||||
messageId = data.getLong(KEY_MESSAGE_ID),
|
||||
attachmentId = AttachmentId(data.getLong(KEY_ATTACHMENT_ID))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@ class MediaPreviewV2Adapter(fragment: Fragment) : FragmentStateAdapter(fragment)
|
||||
|
||||
val contentType = attachment.contentType
|
||||
val args = bundleOf(
|
||||
MediaPreviewFragment.DATA_URI to attachment.uri,
|
||||
MediaPreviewFragment.DATA_URI to attachment.displayUri,
|
||||
MediaPreviewFragment.DATA_CONTENT_TYPE to contentType,
|
||||
MediaPreviewFragment.DATA_SIZE to attachment.size,
|
||||
MediaPreviewFragment.AUTO_PLAY to attachment.videoGif,
|
||||
|
||||
@@ -8,6 +8,7 @@ import android.net.Uri;
|
||||
import com.bumptech.glide.load.data.StreamLocalUriFetcher;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.attachments.AttachmentId;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
@@ -39,6 +40,18 @@ class DecryptableStreamLocalUriFetcher extends StreamLocalUriFetcher {
|
||||
thumbnail.recycle();
|
||||
return thumbnailStream;
|
||||
}
|
||||
if (PartAuthority.isAttachmentUri(uri) && MediaUtil.isVideoType(PartAuthority.getAttachmentContentType(context, uri))) {
|
||||
try {
|
||||
AttachmentId attachmentId = PartAuthority.requireAttachmentId(uri);
|
||||
Uri thumbnailUri = PartAuthority.getAttachmentThumbnailUri(attachmentId);
|
||||
InputStream thumbStream = PartAuthority.getAttachmentThumbnailStream(context, thumbnailUri);
|
||||
if (thumbStream != null) {
|
||||
return thumbStream;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.i(TAG, "Failed to fetch thumbnail", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -56,11 +56,12 @@ public abstract class Slide {
|
||||
|
||||
@Nullable
|
||||
public Uri getUri() {
|
||||
Uri attachmentUri = attachment.getUri();
|
||||
if (attachmentUri != null) {
|
||||
return attachmentUri;
|
||||
}
|
||||
return attachment.getThumbnailUri();
|
||||
return attachment.getUri();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Uri getDisplayUri() {
|
||||
return attachment.getDisplayUri();
|
||||
}
|
||||
|
||||
public @Nullable Uri getPublicUri() {
|
||||
@@ -141,7 +142,8 @@ public abstract class Slide {
|
||||
|
||||
public boolean isPendingDownload() {
|
||||
return getTransferState() == AttachmentTable.TRANSFER_PROGRESS_FAILED ||
|
||||
getTransferState() == AttachmentTable.TRANSFER_PROGRESS_PENDING;
|
||||
getTransferState() == AttachmentTable.TRANSFER_PROGRESS_PENDING ||
|
||||
getTransferState() == AttachmentTable.TRANSFER_RESTORE_OFFLOADED;
|
||||
}
|
||||
|
||||
public int getTransferState() {
|
||||
@@ -160,6 +162,10 @@ public abstract class Slide {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean hasThumbnail() {
|
||||
return attachment.getThumbnailUri() != null;
|
||||
}
|
||||
|
||||
public boolean hasPlayOverlay() {
|
||||
return false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user