Support streamable stories.

This commit is contained in:
Michelle Tang
2026-08-10 16:32:55 -04:00
committed by Greyson Parrelli
parent 32fa56055e
commit d40b4382e9
5 changed files with 54 additions and 11 deletions
@@ -98,6 +98,7 @@ import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.LinkUtil
import org.thoughtcrime.securesms.util.LongClickCopySpan
import org.thoughtcrime.securesms.util.LongClickMovementMethod
import org.thoughtcrime.securesms.util.MediaUtil
import org.thoughtcrime.securesms.util.Projection
import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.fragments.requireListener
@@ -850,7 +851,7 @@ class StoryViewerPageFragment :
private fun markViewedIfAble() {
val post = viewModel.getPost() ?: return
if (post.content.transferState == AttachmentTable.TRANSFER_PROGRESS_DONE) {
if (post.content.transferState == AttachmentTable.TRANSFER_PROGRESS_DONE || post.content.transferState == AttachmentTable.TRANSFER_PROGRESS_STARTED) {
if (isResumed) {
viewModel.markViewed(post)
}
@@ -895,6 +896,7 @@ class StoryViewerPageFragment :
when (post.content.transferState) {
AttachmentTable.TRANSFER_PROGRESS_DONE -> {
Log.d(TAG, "Story content download is done.")
storySlate.moveToState(StorySlateView.State.HIDDEN, post.id)
viewModel.setIsDisplayingSlate(false)
markViewedIfAble()
@@ -908,10 +910,18 @@ class StoryViewerPageFragment :
}
AttachmentTable.TRANSFER_PROGRESS_STARTED -> {
Log.d(TAG, "Story content download is in progress.")
storySlate.moveToState(StorySlateView.State.LOADING, post.id)
sharedViewModel.setContentIsReady()
viewModel.setIsDisplayingSlate(true)
val isStreamable = post.content is StoryPost.Content.AttachmentContent && MediaUtil.isInstantVideoSupported(post.content.attachment)
if (isStreamable) {
Log.d(TAG, "Story content is streamable while download is in progress.")
storySlate.moveToState(StorySlateView.State.HIDDEN, post.id)
viewModel.setIsDisplayingSlate(false)
markViewedIfAble()
} else {
Log.d(TAG, "Story content download is in progress.")
storySlate.moveToState(StorySlateView.State.LOADING, post.id)
sharedViewModel.setContentIsReady()
viewModel.setIsDisplayingSlate(true)
}
}
AttachmentTable.TRANSFER_PROGRESS_FAILED -> {
@@ -554,10 +554,13 @@ public class MediaUtil {
}
public static boolean isInstantVideoSupported(Slide slide) {
final Attachment attachment = slide.asAttachment();
final boolean isIncremental = attachment.getIncrementalDigest() != null;
final boolean hasIncrementalMacChunkSizeDefined = attachment.incrementalMacChunkSize > 0;
final boolean contentTypeSupported = isVideoType(slide.getContentType());
return isInstantVideoSupported(slide.asAttachment());
}
public static boolean isInstantVideoSupported(Attachment attachment) {
final boolean isIncremental = attachment.getIncrementalDigest() != null;
final boolean hasIncrementalMacChunkSizeDefined = attachment.incrementalMacChunkSize > 0;
final boolean contentTypeSupported = isVideoType(attachment.contentType);
return isIncremental && contentTypeSupported && hasIncrementalMacChunkSizeDefined;
}
@@ -17,4 +17,13 @@
app:shutter_background_color="@color/core_black"
app:surface_type="texture_view" />
<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="center"
style="?android:attr/progressBarStyleLarge"
android:indeterminateTint="@color/signal_colorOnSurfaceVariant"
android:indeterminate="true" />
</FrameLayout>
@@ -35,7 +35,7 @@ class TransferControlsTest {
@Before
fun setUp() {
mockkStatic(MediaUtil::class)
every { MediaUtil.isInstantVideoSupported(any()) } returns false
every { MediaUtil.isInstantVideoSupported(any() as Attachment) } returns false
}
@After
@@ -185,7 +185,7 @@ class TransferControlsTest {
@Test
fun `gallery pending playable is corner without item count`() {
every { MediaUtil.isInstantVideoSupported(any()) } returns true
every { MediaUtil.isInstantVideoSupported(any() as Attachment) } returns true
val state = stateOf(listOf(slide(AttachmentTable.TRANSFER_PROGRESS_PENDING), slide(AttachmentTable.TRANSFER_PROGRESS_PENDING)))
val render = TransferControls.deriveRenderState(state) as TransferControlsRenderState.Pending
assertEquals(TransferControls.Placement.CORNER, render.placement)
@@ -58,6 +58,27 @@ class BetterCipherInputStream(
override fun markSupported(): Boolean = false
/**
* Since AES/CBC decryption is sequential, we can't just skip bytes on the underlying ciphertext stream
* so we must process every preceding ciphertext block to keep its internal chaining state correct.
*/
@Throws(IOException::class)
override fun skip(byteCount: Long): Long {
var totalSkipped = 0L
val buffer = ByteArray(4096)
while (totalSkipped < byteCount) {
val toRead = min(buffer.size.toLong(), byteCount - totalSkipped).toInt()
val read = read(buffer, 0, toRead)
if (read == -1) {
break
}
totalSkipped += read
}
return totalSkipped
}
@Throws(IOException::class)
private fun readIncremental(outputBuffer: ByteArray, originalOffset: Int, originalLength: Int): Int {
var offset = originalOffset