Streamable Video.

This commit is contained in:
Nicholas
2023-08-29 16:52:17 -04:00
committed by Nicholas Tinsley
parent 099c94c215
commit 64babe2e42
23 changed files with 290 additions and 125 deletions

View File

@@ -9,6 +9,7 @@ import org.thoughtcrime.securesms.blurhash.BlurHash;
import org.thoughtcrime.securesms.database.AttachmentTable.TransformProperties;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.stickers.StickerLocator;
import org.thoughtcrime.securesms.util.FeatureFlags;
import java.util.Comparator;
@@ -60,7 +61,7 @@ public class DatabaseAttachment extends Attachment {
@Override
@Nullable
public Uri getUri() {
if (hasData) {
if (hasData || (FeatureFlags.instantVideoPlayback() && getIncrementalDigest() != null)) {
return PartAuthority.getAttachmentDataUri(attachmentId);
} else {
return null;

View File

@@ -255,6 +255,14 @@ class ConversationItemThumbnail @JvmOverloads constructor(
state.applyState(thumbnail, album)
}
fun setProgressWheelClickListener(listener: SlideClickListener?) {
state = state.copy(
thumbnailViewState = state.thumbnailViewState.copy(progressWheelClickListener = listener)
)
state.applyState(thumbnail, album)
}
private fun setThumbnailBounds(bounds: IntArray) {
val (minWidth, maxWidth, minHeight, maxHeight) = bounds
state = state.copy(

View File

@@ -31,6 +31,8 @@ data class ConversationItemThumbnailState(
@IgnoredOnParcel
private val downloadClickListener: SlidesClickedListener? = null,
@IgnoredOnParcel
private val progressWheelClickListener: SlideClickListener? = null,
@IgnoredOnParcel
private val longClickListener: OnLongClickListener? = null,
private val visibility: Int = View.GONE,
private val minWidth: Int = -1,
@@ -55,6 +57,7 @@ data class ConversationItemThumbnailState(
thumbnailView.get().setRadii(cornerTopLeft, cornerTopRight, cornerBottomRight, cornerBottomLeft)
thumbnailView.get().setThumbnailClickListener(clickListener)
thumbnailView.get().setDownloadClickListener(downloadClickListener)
thumbnailView.get().setProgressWheelClickListener(progressWheelClickListener)
thumbnailView.get().setOnLongClickListener(longClickListener)
thumbnailView.get().setBounds(minWidth, maxWidth, minHeight, maxHeight)
}

View File

@@ -41,6 +41,7 @@ import org.thoughtcrime.securesms.mms.SlideClickListener;
import org.thoughtcrime.securesms.mms.SlidesClickedListener;
import org.thoughtcrime.securesms.mms.VideoSlide;
import org.thoughtcrime.securesms.stories.StoryTextPostModel;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
@@ -79,11 +80,12 @@ public class ThumbnailView extends FrameLayout {
private final CornerMask cornerMask;
private ThumbnailViewTransferControlsState transferControlsState = new ThumbnailViewTransferControlsState();
private ThumbnailViewTransferControlsState transferControlsState = new ThumbnailViewTransferControlsState();
private Stub<TransferControlView> transferControlViewStub;
private SlideClickListener thumbnailClickListener = null;
private SlidesClickedListener downloadClickListener = null;
private Slide slide = null;
private SlideClickListener thumbnailClickListener = null;
private SlidesClickedListener downloadClickListener = null;
private SlideClickListener progressWheelClickListener = null;
private Slide slide = null;
public ThumbnailView(Context context) {
@@ -366,6 +368,11 @@ public class ThumbnailView extends FrameLayout {
transferControlsState = transferControlsState.withSlide(slide)
.withDownloadClickListener(new DownloadClickDispatcher());
if (FeatureFlags.instantVideoPlayback()) {
transferControlsState = transferControlsState.withProgressWheelClickListener(new ProgressWheelClickDispatcher());
}
transferControlsState.applyState(transferControlViewStub);
} else {
transferControlViewStub.setVisibility(View.GONE);
@@ -518,6 +525,10 @@ public class ThumbnailView extends FrameLayout {
this.downloadClickListener = listener;
}
public void setProgressWheelClickListener(SlideClickListener listener) {
this.progressWheelClickListener = listener;
}
public void clear(GlideRequests glideRequests) {
glideRequests.clear(image);
image.setImageDrawable(null);
@@ -659,6 +670,18 @@ public class ThumbnailView extends FrameLayout {
}
}
private class ProgressWheelClickDispatcher implements View.OnClickListener {
@Override
public void onClick(View view) {
Log.i(TAG, "onClick() for progress wheel");
if (progressWheelClickListener != null && slide != null) {
progressWheelClickListener.onClick(view, slide);
} else {
Log.w(TAG, "Received a progress wheel click, but unable to execute it. slide: " + slide + " progressWheelClickListener: " + progressWheelClickListener);
}
}
}
private static class BlurHashClearListener implements ListenableFuture.Listener<Boolean> {
private final GlideRequests glideRequests;

View File

@@ -12,6 +12,7 @@ data class ThumbnailViewTransferControlsState(
val isClickable: Boolean = true,
val slide: Slide? = null,
val downloadClickedListener: OnClickListener? = null,
val progressWheelClickedListener: OnClickListener? = null,
val showDownloadText: Boolean = true
) {
@@ -19,6 +20,7 @@ data class ThumbnailViewTransferControlsState(
fun withClickable(isClickable: Boolean): ThumbnailViewTransferControlsState = copy(isClickable = isClickable)
fun withSlide(slide: Slide?): ThumbnailViewTransferControlsState = copy(slide = slide)
fun withDownloadClickListener(downloadClickedListener: OnClickListener): ThumbnailViewTransferControlsState = copy(downloadClickedListener = downloadClickedListener)
fun withProgressWheelClickListener(progressWheelClickedListener: OnClickListener): ThumbnailViewTransferControlsState = copy(progressWheelClickedListener = progressWheelClickedListener)
fun withDownloadText(showDownloadText: Boolean): ThumbnailViewTransferControlsState = copy(showDownloadText = showDownloadText)
fun applyState(transferControlView: Stub<TransferControlView>) {
@@ -29,6 +31,7 @@ data class ThumbnailViewTransferControlsState(
transferControlView.get().setSlide(slide)
}
transferControlView.get().setDownloadClickListener(downloadClickedListener)
transferControlView.get().setProgressWheelClickListener(progressWheelClickedListener)
transferControlView.get().setShowDownloadText(showDownloadText)
}
}

View File

@@ -30,6 +30,7 @@ import java.util.Map;
public final class TransferControlView extends FrameLayout {
private static final String TAG = "TransferControlView";
private static final int UPLOAD_TASK_WEIGHT = 1;
/**
@@ -152,6 +153,10 @@ public final class TransferControlView extends FrameLayout {
downloadDetails.setOnClickListener(listener);
}
public void setProgressWheelClickListener(final @Nullable OnClickListener listener) {
progressWheel.setOnClickListener(listener);
}
public void clear() {
clearAnimation();
setVisibility(GONE);
@@ -247,13 +252,14 @@ public final class TransferControlView extends FrameLayout {
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
public void onEventAsync(final PartProgressEvent event) {
if (networkProgress.containsKey(event.attachment)) {
final Attachment attachment = event.attachment;
if (networkProgress.containsKey(attachment)) {
float proportionCompleted = ((float) event.progress) / event.total;
if (event.type == PartProgressEvent.Type.COMPRESSION) {
compresssionProgress.put(event.attachment, proportionCompleted);
compresssionProgress.put(attachment, proportionCompleted);
} else {
networkProgress.put(event.attachment, proportionCompleted);
networkProgress.put(attachment, proportionCompleted);
}
progressWheel.setInstantProgress(calculateProgress(networkProgress, compresssionProgress));

View File

@@ -135,6 +135,7 @@ import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.InterceptableLongClickCopyLinkSpan;
import org.thoughtcrime.securesms.util.LongClickMovementMethod;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.MessageRecordUtil;
import org.thoughtcrime.securesms.util.PlaceholderURLSpan;
import org.thoughtcrime.securesms.util.Projection;
@@ -240,6 +241,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
private final PassthroughClickListener passthroughClickListener = new PassthroughClickListener();
private final AttachmentDownloadClickListener downloadClickListener = new AttachmentDownloadClickListener();
private final ProgressWheelClickListener progressWheelClickListener = new ProgressWheelClickListener();
private final SlideClickPassthroughListener singleDownloadClickListener = new SlideClickPassthroughListener(downloadClickListener);
private final SharedContactEventListener sharedContactEventListener = new SharedContactEventListener();
private final SharedContactClickListener sharedContactClickListener = new SharedContactClickListener();
@@ -1162,6 +1164,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
mediaThumbnailStub.require().setImageResource(glideRequests, Collections.singletonList(new ImageSlide(linkPreview.getThumbnail().get())), showControls, false);
mediaThumbnailStub.require().setThumbnailClickListener(new LinkPreviewThumbnailClickListener());
mediaThumbnailStub.require().setDownloadClickListener(downloadClickListener);
mediaThumbnailStub.require().setProgressWheelClickListener(progressWheelClickListener);
mediaThumbnailStub.require().setOnLongClickListener(passthroughClickListener);
linkPreviewStub.get().setLinkPreview(glideRequests, linkPreview, false);
@@ -1301,6 +1304,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
false);
mediaThumbnailStub.require().setThumbnailClickListener(new ThumbnailClickListener());
mediaThumbnailStub.require().setDownloadClickListener(downloadClickListener);
mediaThumbnailStub.require().setProgressWheelClickListener(progressWheelClickListener);
mediaThumbnailStub.require().setOnLongClickListener(passthroughClickListener);
mediaThumbnailStub.require().setOnClickListener(passthroughClickListener);
mediaThumbnailStub.require().showShade(messageRecord.isDisplayBodyEmpty(getContext()) && !hasExtraText(messageRecord));
@@ -1545,6 +1549,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
messageBody.setSpan(new MentionClickableSpan(RecipientId.from(annotation.getValue())), messageBody.getSpanStart(annotation), messageBody.getSpanEnd(annotation), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
private void setStatusIcons(MessageRecord messageRecord, boolean hasWallpaper) {
bodyText.setCompoundDrawablesWithIntrinsicBounds(0, 0, messageRecord.isKeyExchange() ? R.drawable.ic_menu_login : 0, 0);
@@ -2429,6 +2434,20 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
}
}
private class ProgressWheelClickListener implements SlideClickListener {
@Override
public void onClick(View v, Slide slide) {
final boolean isIncremental = slide.asAttachment().getIncrementalDigest() != null;
final boolean contentTypeSupported = MediaUtil.isVideoType(slide.getContentType());
if (FeatureFlags.instantVideoPlayback() && isIncremental && contentTypeSupported) {
launchMediaPreview(v, slide);
} else {
Log.d(TAG, "Non-eligible slide clicked: " + "\tisIncremental: " + isIncremental + "\tcontentTypeSupported: " + contentTypeSupported);
}
}
}
private class SlideClickPassthroughListener implements SlideClickListener {
private final SlidesClickedListener original;
@@ -2462,34 +2481,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
} else if (!canPlayContent && mediaItem != null && eventListener != null) {
eventListener.onPlayInlineContent(conversationMessage);
} else if (MediaPreviewV2Fragment.isContentTypeSupported(slide.getContentType()) && slide.getUri() != null) {
if (eventListener == null) {
return;
}
MediaIntentFactory.MediaPreviewArgs args = new MediaIntentFactory.MediaPreviewArgs(
messageRecord.getThreadId(),
messageRecord.getTimestamp(),
slide.getUri(),
slide.getContentType(),
slide.asAttachment().getSize(),
slide.getCaption().orElse(null),
false,
false,
false,
false,
MediaTable.Sorting.Newest,
slide.isVideoGif(),
new MediaIntentFactory.SharedElementArgs(
slide.asAttachment().getWidth(),
slide.asAttachment().getHeight(),
mediaThumbnailStub.require().getCorners().getTopLeft(),
mediaThumbnailStub.require().getCorners().getTopRight(),
mediaThumbnailStub.require().getCorners().getBottomRight(),
mediaThumbnailStub.require().getCorners().getBottomLeft()
),
false);
MediaPreviewCache.INSTANCE.setDrawable(((ThumbnailView) v).getImageDrawable());
eventListener.goToMediaPreview(ConversationItem.this, v, args);
launchMediaPreview(v, slide);
} else if (slide.getUri() != null) {
Log.i(TAG, "Clicked: " + slide.getUri() + " , " + slide.getContentType());
Uri publicUri = PartAuthority.getAttachmentPublicUri(slide.getUri());
@@ -2526,6 +2518,47 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
}
}
private void launchMediaPreview(View v, Slide slide) {
if (eventListener == null) {
Log.w(TAG, "Could not launch media preview for item: eventListener was null");
return;
}
Uri mediaUri = slide.getUri();
if (mediaUri == null) {
Log.w(TAG, "Could not launch media preview for item: uri was null");
return;
}
MediaIntentFactory.MediaPreviewArgs args = new MediaIntentFactory.MediaPreviewArgs(
messageRecord.getThreadId(),
messageRecord.getTimestamp(),
mediaUri,
slide.getContentType(),
slide.asAttachment().getSize(),
slide.getCaption().orElse(null),
false,
false,
false,
false,
MediaTable.Sorting.Newest,
slide.isVideoGif(),
new MediaIntentFactory.SharedElementArgs(
slide.asAttachment().getWidth(),
slide.asAttachment().getHeight(),
mediaThumbnailStub.require().getCorners().getTopLeft(),
mediaThumbnailStub.require().getCorners().getTopRight(),
mediaThumbnailStub.require().getCorners().getBottomRight(),
mediaThumbnailStub.require().getCorners().getBottomLeft()
),
false);
if (v instanceof ThumbnailView) {
MediaPreviewCache.INSTANCE.setDrawable(((ThumbnailView) v).getImageDrawable());
}
eventListener.goToMediaPreview(ConversationItem.this, v, args);
}
private class PassthroughClickListener implements View.OnLongClickListener, View.OnClickListener {
@Override

View File

@@ -1343,39 +1343,39 @@ public class AttachmentTable extends DatabaseTable {
private @NonNull DatabaseAttachment getAttachment(@NonNull Cursor cursor) {
String contentType = cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_TYPE));
return new DatabaseAttachment(new AttachmentId(cursor.getLong(cursor.getColumnIndexOrThrow(ROW_ID)),
cursor.getLong(cursor.getColumnIndexOrThrow(UNIQUE_ID))),
cursor.getLong(cursor.getColumnIndexOrThrow(MMS_ID)),
!cursor.isNull(cursor.getColumnIndexOrThrow(DATA)),
MediaUtil.isImageType(contentType) || MediaUtil.isVideoType(contentType),
contentType,
cursor.getInt(cursor.getColumnIndexOrThrow(TRANSFER_STATE)),
cursor.getLong(cursor.getColumnIndexOrThrow(SIZE)),
cursor.getString(cursor.getColumnIndexOrThrow(FILE_NAME)),
cursor.getInt(cursor.getColumnIndexOrThrow(CDN_NUMBER)),
cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_LOCATION)),
cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_DISPOSITION)),
cursor.getString(cursor.getColumnIndexOrThrow(NAME)),
cursor.getBlob(cursor.getColumnIndexOrThrow(DIGEST)),
cursor.getBlob(cursor.getColumnIndexOrThrow(MAC_DIGEST)),
cursor.getString(cursor.getColumnIndexOrThrow(FAST_PREFLIGHT_ID)),
cursor.getInt(cursor.getColumnIndexOrThrow(VOICE_NOTE)) == 1,
cursor.getInt(cursor.getColumnIndexOrThrow(BORDERLESS)) == 1,
cursor.getInt(cursor.getColumnIndexOrThrow(VIDEO_GIF)) == 1,
cursor.getInt(cursor.getColumnIndexOrThrow(WIDTH)),
cursor.getInt(cursor.getColumnIndexOrThrow(HEIGHT)),
cursor.getInt(cursor.getColumnIndexOrThrow(QUOTE)) == 1,
cursor.getString(cursor.getColumnIndexOrThrow(CAPTION)),
cursor.getInt(cursor.getColumnIndexOrThrow(STICKER_ID)) >= 0
? new StickerLocator(CursorUtil.requireString(cursor, STICKER_PACK_ID),
CursorUtil.requireString(cursor, STICKER_PACK_KEY),
CursorUtil.requireInt(cursor, STICKER_ID),
CursorUtil.requireString(cursor, STICKER_EMOJI))
: null,
MediaUtil.isAudioType(contentType) ? null : BlurHash.parseOrNull(cursor.getString(cursor.getColumnIndexOrThrow(VISUAL_HASH))),
MediaUtil.isAudioType(contentType) ? AudioHash.parseOrNull(cursor.getString(cursor.getColumnIndexOrThrow(VISUAL_HASH))) : null,
TransformProperties.parse(cursor.getString(cursor.getColumnIndexOrThrow(TRANSFORM_PROPERTIES))),
cursor.getInt(cursor.getColumnIndexOrThrow(DISPLAY_ORDER)),
cursor.getLong(cursor.getColumnIndexOrThrow(UPLOAD_TIMESTAMP)));
cursor.getLong(cursor.getColumnIndexOrThrow(UNIQUE_ID))),
cursor.getLong(cursor.getColumnIndexOrThrow(MMS_ID)),
!cursor.isNull(cursor.getColumnIndexOrThrow(DATA)),
MediaUtil.isImageType(contentType) || MediaUtil.isVideoType(contentType),
contentType,
cursor.getInt(cursor.getColumnIndexOrThrow(TRANSFER_STATE)),
cursor.getLong(cursor.getColumnIndexOrThrow(SIZE)),
cursor.getString(cursor.getColumnIndexOrThrow(FILE_NAME)),
cursor.getInt(cursor.getColumnIndexOrThrow(CDN_NUMBER)),
cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_LOCATION)),
cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_DISPOSITION)),
cursor.getString(cursor.getColumnIndexOrThrow(NAME)),
cursor.getBlob(cursor.getColumnIndexOrThrow(DIGEST)),
cursor.getBlob(cursor.getColumnIndexOrThrow(MAC_DIGEST)),
cursor.getString(cursor.getColumnIndexOrThrow(FAST_PREFLIGHT_ID)),
cursor.getInt(cursor.getColumnIndexOrThrow(VOICE_NOTE)) == 1,
cursor.getInt(cursor.getColumnIndexOrThrow(BORDERLESS)) == 1,
cursor.getInt(cursor.getColumnIndexOrThrow(VIDEO_GIF)) == 1,
cursor.getInt(cursor.getColumnIndexOrThrow(WIDTH)),
cursor.getInt(cursor.getColumnIndexOrThrow(HEIGHT)),
cursor.getInt(cursor.getColumnIndexOrThrow(QUOTE)) == 1,
cursor.getString(cursor.getColumnIndexOrThrow(CAPTION)),
cursor.getInt(cursor.getColumnIndexOrThrow(STICKER_ID)) >= 0
? new StickerLocator(CursorUtil.requireString(cursor, STICKER_PACK_ID),
CursorUtil.requireString(cursor, STICKER_PACK_KEY),
CursorUtil.requireInt(cursor, STICKER_ID),
CursorUtil.requireString(cursor, STICKER_EMOJI))
: null,
MediaUtil.isAudioType(contentType) ? null : BlurHash.parseOrNull(cursor.getString(cursor.getColumnIndexOrThrow(VISUAL_HASH))),
MediaUtil.isAudioType(contentType) ? AudioHash.parseOrNull(cursor.getString(cursor.getColumnIndexOrThrow(VISUAL_HASH))) : null,
TransformProperties.parse(cursor.getString(cursor.getColumnIndexOrThrow(TRANSFORM_PROPERTIES))),
cursor.getInt(cursor.getColumnIndexOrThrow(DISPLAY_ORDER)),
cursor.getLong(cursor.getColumnIndexOrThrow(UPLOAD_TIMESTAMP)));
}
private AttachmentId insertAttachment(long mmsId, Attachment attachment, boolean quote)
@@ -1514,15 +1514,28 @@ public class AttachmentTable extends DatabaseTable {
@RequiresApi(23)
public @Nullable MediaDataSource mediaDataSourceFor(@NonNull AttachmentId attachmentId) {
public @Nullable MediaDataSource mediaDataSourceFor(@NonNull AttachmentId attachmentId, Boolean allowReadingFromTempFile) {
DataInfo dataInfo = getAttachmentDataFileInfo(attachmentId, DATA);
if (dataInfo == null) {
Log.w(TAG, "No data file found for video attachment...");
return null;
if (dataInfo != null) {
return EncryptedMediaDataSource.createFor(attachmentSecret, dataInfo.file, dataInfo.random, dataInfo.length);
}
return EncryptedMediaDataSource.createFor(attachmentSecret, dataInfo.file, dataInfo.random, dataInfo.length);
if (allowReadingFromTempFile) {
Log.d(TAG, "Completed data file not found for video attachment, checking for in-progress files.");
SQLiteDatabase database = databaseHelper.getSignalReadableDatabase();
File transferFile = getTransferFile(database, attachmentId);
if (transferFile != null) {
return EncryptedMediaDataSource.createForDiskBlob(attachmentSecret, transferFile);
}
}
Log.w(TAG, "No data file found for video attachment!");
return null;
}
public void duplicateAttachmentsForMessage(long destinationMessageId, long sourceMessageId, Collection<Long> excludedIds) {

View File

@@ -8,6 +8,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
@@ -68,8 +69,7 @@ class MediaTable internal constructor(context: Context?, databaseHelper: SignalD
) AND
(%s) AND
${MessageTable.VIEW_ONCE} = 0 AND
${MessageTable.STORY_TYPE} = 0 AND
${AttachmentTable.DATA} IS NOT NULL AND
${MessageTable.STORY_TYPE} = 0 AND
${MessageTable.LATEST_REVISION_ID} IS NULL AND
(
${AttachmentTable.QUOTE} = 0 OR
@@ -98,32 +98,54 @@ class MediaTable internal constructor(context: Context?, databaseHelper: SignalD
private val GALLERY_MEDIA_QUERY = String.format(
BASE_MEDIA_QUERY,
"""
${AttachmentTable.DATA} IS NOT NULL AND
${AttachmentTable.CONTENT_TYPE} NOT LIKE 'image/svg%' AND
(${AttachmentTable.CONTENT_TYPE} LIKE 'image/%' OR ${AttachmentTable.CONTENT_TYPE} LIKE 'video/%')
"""
)
private val AUDIO_MEDIA_QUERY = String.format(BASE_MEDIA_QUERY, "${AttachmentTable.CONTENT_TYPE} LIKE 'audio/%'")
private val GALLERY_MEDIA_QUERY_INCLUDING_TEMP_VIDEOS = String.format(
BASE_MEDIA_QUERY,
"""
${AttachmentTable.DATA} IS NOT NULL OR (${AttachmentTable.CONTENT_TYPE} LIKE 'video/%' AND ${AttachmentTable.MAC_DIGEST} IS NOT NULL) AND
${AttachmentTable.CONTENT_TYPE} NOT LIKE 'image/svg%' AND
(${AttachmentTable.CONTENT_TYPE} LIKE 'image/%' OR ${AttachmentTable.CONTENT_TYPE} LIKE 'video/%')
"""
)
private val AUDIO_MEDIA_QUERY = String.format(
BASE_MEDIA_QUERY,
"""
${AttachmentTable.DATA} IS NOT NULL AND
${AttachmentTable.CONTENT_TYPE} LIKE 'audio/%'
"""
)
private val ALL_MEDIA_QUERY = String.format(BASE_MEDIA_QUERY, "${AttachmentTable.CONTENT_TYPE} NOT LIKE 'text/x-signal-plain'")
private val DOCUMENT_MEDIA_QUERY = String.format(
BASE_MEDIA_QUERY,
"""
${AttachmentTable.CONTENT_TYPE} LIKE 'image/svg%' OR
${AttachmentTable.DATA} IS NOT NULL AND
(
${AttachmentTable.CONTENT_TYPE} NOT LIKE 'image/%' AND
${AttachmentTable.CONTENT_TYPE} NOT LIKE 'video/%' AND
${AttachmentTable.CONTENT_TYPE} NOT LIKE 'audio/%' AND
${AttachmentTable.CONTENT_TYPE} NOT LIKE 'text/x-signal-plain'
${AttachmentTable.CONTENT_TYPE} LIKE 'image/svg%' OR
(
${AttachmentTable.CONTENT_TYPE} NOT LIKE 'image/%' AND
${AttachmentTable.CONTENT_TYPE} NOT LIKE 'video/%' AND
${AttachmentTable.CONTENT_TYPE} NOT LIKE 'audio/%' AND
${AttachmentTable.CONTENT_TYPE} NOT LIKE 'text/x-signal-plain'
)
)"""
)
private fun applyEqualityOperator(threadId: Long, query: String): String {
return query.replace("__EQUALITY__", if (threadId == ALL_THREADS.toLong()) "!=" else "=")
}
}
fun getGalleryMediaForThread(threadId: Long, sorting: Sorting): Cursor {
val query = sorting.applyToQuery(applyEqualityOperator(threadId, GALLERY_MEDIA_QUERY))
val query = if (FeatureFlags.instantVideoPlayback()) {
sorting.applyToQuery(applyEqualityOperator(threadId, GALLERY_MEDIA_QUERY_INCLUDING_TEMP_VIDEOS))
} else {
sorting.applyToQuery(applyEqualityOperator(threadId, GALLERY_MEDIA_QUERY))
}
val args = arrayOf(threadId.toString() + "")
return readableDatabase.rawQuery(query, args)
}

View File

@@ -209,7 +209,7 @@ public final class AttachmentCompressionJob extends BaseJob {
notification.setIndeterminateProgress();
try (MediaDataSource dataSource = attachmentDatabase.mediaDataSourceFor(attachment.getAttachmentId())) {
try (MediaDataSource dataSource = attachmentDatabase.mediaDataSourceFor(attachment.getAttachmentId(), false)) {
if (dataSource == null) {
throw new UndeliverableMessageException("Cannot get media data source for attachment.");
}

View File

@@ -41,7 +41,7 @@ public final class DecryptableUriMediaInput {
throw new AssertionError();
}
MediaDataSource mediaDataSource = SignalDatabase.attachments().mediaDataSourceFor(partId);
MediaDataSource mediaDataSource = SignalDatabase.attachments().mediaDataSourceFor(partId, true);
if (mediaDataSource == null) {
throw new AssertionError();

View File

@@ -66,9 +66,7 @@ class MediaPreviewRepository {
for (i in 0..limit) {
val element = MediaTable.MediaRecord.from(cursor)
if (element != null) {
mediaRecords.add(element)
}
mediaRecords.add(element)
if (!cursor.moveToNext()) {
break
}

View File

@@ -3,12 +3,14 @@ package org.thoughtcrime.securesms.mediapreview
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import androidx.viewpager2.adapter.FragmentStateAdapter
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.attachments.Attachment
import org.thoughtcrime.securesms.mediasend.Media
import org.thoughtcrime.securesms.util.MediaUtil
import org.thoughtcrime.securesms.util.adapter.StableIdGenerator
class MediaPreviewV2Adapter(fragment: Fragment) : FragmentStateAdapter(fragment) {
private val TAG = Log.tag(MediaPreviewV2Adapter::class.java)
private var items: List<Attachment> = listOf()
private val stableIdGenerator = StableIdGenerator<Attachment>()
private val currentIdSet: HashSet<Long> = HashSet()

View File

@@ -6,6 +6,7 @@ import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -42,6 +43,12 @@ public final class VideoMediaPreviewFragment extends MediaPreviewFragment {
View itemView = inflater.inflate(R.layout.media_preview_video_fragment, container, false);
Bundle arguments = requireArguments();
Uri uri = arguments.getParcelable(DATA_URI);
if (uri == null) {
Log.w(TAG, "Media URI was null.");
Toast.makeText(requireContext(), R.string.MediaPreviewActivity_media_no_longer_available, Toast.LENGTH_LONG).show();
requireActivity().finish();
return itemView;
}
String contentType = arguments.getString(DATA_CONTENT_TYPE);
long size = arguments.getLong(DATA_SIZE);
boolean autoPlay = arguments.getBoolean(AUTO_PLAY);

View File

@@ -7,6 +7,7 @@ import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.data.DataFetcher;
import org.signal.core.util.logging.Log;
import org.signal.libsignal.protocol.InvalidMacException;
import org.signal.libsignal.protocol.InvalidMessageException;
import org.whispersystems.signalservice.api.crypto.AttachmentCipherInputStream;
@@ -39,7 +40,7 @@ class AttachmentStreamLocalUriFetcher implements DataFetcher<InputStream> {
public void loadData(@NonNull Priority priority, @NonNull DataCallback<? super InputStream> callback) {
try {
if (!digest.isPresent()) throw new InvalidMessageException("No attachment digest!");
is = AttachmentCipherInputStream.createForAttachment(attachment, plaintextLength, key, digest.get(), incrementalDigest.get());
is = AttachmentCipherInputStream.createForAttachment(attachment, plaintextLength, key, digest.get(), incrementalDigest.orElse(null));
callback.onDataReady(is);
} catch (IOException | InvalidMessageException e) {
callback.onLoadFailed(e);

View File

@@ -113,6 +113,7 @@ public final class FeatureFlags {
private static final String PROMPT_FOR_NOTIFICATION_CONFIG = "android.logs.promptNotificationsConfig";
public static final String PROMPT_BATTERY_SAVER = "android.promptBatterySaver";
public static final String USERNAMES = "android.usernames";
public static final String INSTANT_VIDEO_PLAYBACK = "android.instantVideoPlayback";
/**
* We will only store remote values for flags in this set. If you want a flag to be controllable
@@ -177,7 +178,8 @@ public final class FeatureFlags {
PROMPT_FOR_NOTIFICATION_LOGS,
PROMPT_FOR_NOTIFICATION_CONFIG,
PROMPT_BATTERY_SAVER,
USERNAMES
USERNAMES,
INSTANT_VIDEO_PLAYBACK
);
@VisibleForTesting
@@ -629,6 +631,14 @@ public final class FeatureFlags {
}
}
/**
* Allow the video players to read from the temporary download files for attachments.
* @return whether this functionality is enabled.
*/
public static boolean instantVideoPlayback() {
return getBoolean(INSTANT_VIDEO_PLAYBACK, false);
}
public static String promptForDelayedNotificationLogs() {
return getString(PROMPT_FOR_NOTIFICATION_LOGS, "*");
}
@@ -641,6 +651,7 @@ public final class FeatureFlags {
return getString(PROMPT_BATTERY_SAVER, "*");
}
/** Only for rendering debug info. */
public static synchronized @NonNull Map<String, Object> getMemoryValues() {
return new TreeMap<>(REMOTE_VALUES);

View File

@@ -446,7 +446,7 @@ public class MediaUtil {
{
try {
AttachmentId attachmentId = PartAuthority.requireAttachmentId(uri);
MediaDataSource source = SignalDatabase.attachments().mediaDataSourceFor(attachmentId);
MediaDataSource source = SignalDatabase.attachments().mediaDataSourceFor(attachmentId, false);
return extractFrame(source, timeUs);
} catch (IOException e) {
Log.w(TAG, "Failed to extract frame for URI: " + uri, e);

View File

@@ -1,24 +1,29 @@
package org.thoughtcrime.securesms.video.exo;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.OptIn;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.datasource.DataSource;
import androidx.media3.datasource.DataSpec;
import androidx.media3.datasource.TransferListener;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.signal.core.util.logging.Log;
import org.signal.libsignal.protocol.InvalidMessageException;
import org.signal.libsignal.protocol.incrementalmac.ChunkSizeChoice;
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
import org.thoughtcrime.securesms.database.AttachmentTable;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.mms.PartUriParser;
import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.whispersystems.signalservice.api.crypto.AttachmentCipherInputStream;
import java.io.EOFException;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
@@ -28,14 +33,13 @@ import java.util.Map;
@OptIn(markerClass = UnstableApi.class)
class PartDataSource implements DataSource {
private final @NonNull Context context;
private final String TAG = Log.tag(PartDataSource.class);
private final @Nullable TransferListener listener;
private Uri uri;
private InputStream inputSteam;
private InputStream inputStream;
PartDataSource(@NonNull Context context, @Nullable TransferListener listener) {
this.context = context.getApplicationContext();
PartDataSource(@Nullable TransferListener listener) {
this.listener = listener;
}
@@ -47,13 +51,42 @@ class PartDataSource implements DataSource {
public long open(DataSpec dataSpec) throws IOException {
this.uri = dataSpec.uri;
AttachmentTable attachmentDatabase = SignalDatabase.attachments();
PartUriParser partUri = new PartUriParser(uri);
Attachment attachment = attachmentDatabase.getAttachment(partUri.getPartId());
AttachmentTable attachmentDatabase = SignalDatabase.attachments();
PartUriParser partUri = new PartUriParser(uri);
DatabaseAttachment attachment = attachmentDatabase.getAttachment(partUri.getPartId());
if (attachment == null) throw new IOException("Attachment not found");
this.inputSteam = attachmentDatabase.getAttachmentStream(partUri.getPartId(), dataSpec.position);
final boolean hasIncrementalDigest = attachment.getIncrementalDigest() != null;
final boolean inProgress = attachment.isInProgress();
final String attachmentKey = attachment.getKey();
final boolean hasData = attachment.hasData();
if (inProgress && !hasData && hasIncrementalDigest && attachmentKey != null && FeatureFlags.instantVideoPlayback()) {
final byte[] decode = Base64.decode(attachmentKey);
final File transferFile = attachmentDatabase.getOrCreateTransferFile(attachment.getAttachmentId());
try {
this.inputStream = AttachmentCipherInputStream.createForAttachment(transferFile, attachment.getSize(), decode, attachment.getDigest(), attachment.getIncrementalDigest());
long skipped = 0;
while (skipped < dataSpec.position) {
skipped += this.inputStream.read();
}
Log.d(TAG, "Successfully loaded partial attachment file.");
} catch (InvalidMessageException e) {
throw new IOException("Error decrypting attachment stream!", e);
}
} else if (!inProgress || hasData) {
this.inputStream = attachmentDatabase.getAttachmentStream(partUri.getPartId(), dataSpec.position);
Log.d(TAG, "Successfully loaded completed attachment file.");
} else {
throw new IOException("Ineligible " + attachment.getAttachmentId().toString()
+ "\nTransfer state: " + attachment.getTransferState()
+ "\nIncremental Digest Present: " + hasIncrementalDigest
+ "\nAttachment Key Non-Empty: " + (attachmentKey != null && !attachmentKey.isEmpty()));
}
if (listener != null) {
listener.onTransferStart(this, dataSpec, false);
@@ -66,7 +99,7 @@ class PartDataSource implements DataSource {
@Override
public int read(@NonNull byte[] buffer, int offset, int readLength) throws IOException {
int read = inputSteam.read(buffer, offset, readLength);
int read = inputStream.read(buffer, offset, readLength);
if (read > 0 && listener != null) {
listener.onBytesTransferred(this, null, false, read);
@@ -87,6 +120,6 @@ class PartDataSource implements DataSource {
@Override
public void close() throws IOException {
inputSteam.close();
if (inputStream != null) inputStream.close();
}
}

View File

@@ -116,7 +116,7 @@ import okhttp3.OkHttpClient;
@Override
public @NonNull SignalDataSource createDataSource() {
return new SignalDataSource(new DefaultDataSourceFactory(context, "GenericUserAgent", null).createDataSource(),
new PartDataSource(context, listener),
new PartDataSource(listener),
new BlobDataSource(context, listener),
okHttpClient != null ? new ChunkedDataSource(okHttpClient, listener) : null);
}

View File

@@ -159,7 +159,7 @@ public class SignalServiceMessageReceiver {
if (!pointer.getDigest().isPresent()) throw new InvalidMessageException("No attachment digest!");
socket.retrieveAttachment(pointer.getCdnNumber(), pointer.getRemoteId(), destination, maxSizeBytes, listener);
return AttachmentCipherInputStream.createForAttachment(destination, pointer.getSize().orElse(0), pointer.getKey(), pointer.getDigest().get(), pointer.getIncrementalDigest().orElse(new byte[0]));
return AttachmentCipherInputStream.createForAttachment(destination, pointer.getSize().orElse(0), pointer.getKey(), pointer.getDigest().get(), null);
}
public InputStream retrieveSticker(byte[] packId, byte[] packKey, int stickerId)

View File

@@ -69,22 +69,22 @@ public class AttachmentCipherInputStream extends FilterInputStream {
throw new InvalidMacException("Missing digest!");
}
try (FileInputStream fin = new FileInputStream(file)) {
verifyMac(fin, file.length(), mac, digest);
}
final FileInputStream innerStream = new FileInputStream(file);
final InputStream wrappedStream;
boolean hasIncrementalMac = incrementalDigest != null && incrementalDigest.length > 0;
InputStream wrap = !hasIncrementalMac ? innerStream
: new IncrementalMacInputStream(
innerStream,
parts[1],
ChunkSizeChoice.inferChunkSize(Math.max(Math.toIntExact(file.length()), 1)),
incrementalDigest);
InputStream inputStream = new AttachmentCipherInputStream(wrap, parts[0], file.length() - BLOCK_SIZE - mac.getMacLength());
if (!hasIncrementalMac) {
try (FileInputStream macVerificationStream = new FileInputStream(file)) {
verifyMac(macVerificationStream, file.length(), mac, digest);
}
wrappedStream = new FileInputStream(file);
} else {
wrappedStream = new IncrementalMacInputStream(
new FileInputStream(file),
parts[1],
ChunkSizeChoice.inferChunkSize(Math.toIntExact(plaintextLength)),
incrementalDigest);
}
InputStream inputStream = new AttachmentCipherInputStream(wrappedStream, parts[0], file.length() - BLOCK_SIZE - mac.getMacLength());
if (plaintextLength != 0) {
inputStream = new ContentLengthInputStream(inputStream, plaintextLength);

View File

@@ -81,7 +81,7 @@ class DigestingRequestBody(
return if (contentLength > 0) contentLength - contentStart else -1
}
fun getAttachmentDigest() = AttachmentDigest(transmittedDigest, incrementalDigest)
fun getAttachmentDigest(): AttachmentDigest = AttachmentDigest(transmittedDigest, incrementalDigest)
private fun logMessage(actual: Long, expected: Long): String {
val difference = actual - expected

View File

@@ -67,7 +67,7 @@ public final class AttachmentCipherTest {
try {
byte[] key = Util.getSecretBytes(64);
byte[] plaintextInput = "Gwen Stacy".getBytes();
EncryptResult encryptResult = encryptData(plaintextInput, key, true);
EncryptResult encryptResult = encryptData(plaintextInput, key, false);
byte[] badKey = new byte[64];
cipherFile = writeToFile(encryptResult.ciphertext);
@@ -92,7 +92,7 @@ public final class AttachmentCipherTest {
try {
byte[] key = Util.getSecretBytes(64);
byte[] plaintextInput = "Mary Jane Watson".getBytes();
EncryptResult encryptResult = encryptData(plaintextInput, key, true);
EncryptResult encryptResult = encryptData(plaintextInput, key, false);
byte[] badDigest = new byte[32];
cipherFile = writeToFile(encryptResult.ciphertext);
@@ -213,7 +213,7 @@ public final class AttachmentCipherTest {
try {
byte[] key = Util.getSecretBytes(64);
byte[] plaintextInput = "Uncle Ben".getBytes();
EncryptResult encryptResult = encryptData(plaintextInput, key, true);
EncryptResult encryptResult = encryptData(plaintextInput, key, false);
byte[] badMacCiphertext = Arrays.copyOf(encryptResult.ciphertext, encryptResult.ciphertext.length);
badMacCiphertext[badMacCiphertext.length - 1] += 1;
@@ -221,6 +221,7 @@ public final class AttachmentCipherTest {
cipherFile = writeToFile(badMacCiphertext);
AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.length, key, encryptResult.digest, encryptResult.incrementalDigest);
fail();
} catch (InvalidMessageException e) {
hitCorrectException = true;
} finally {