diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/ThumbnailView.java b/app/src/main/java/org/thoughtcrime/securesms/components/ThumbnailView.java index fcb693e79d..614b4f4cd8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/ThumbnailView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/ThumbnailView.java @@ -36,6 +36,7 @@ import com.bumptech.glide.request.Request; import com.bumptech.glide.request.RequestListener; import com.bumptech.glide.request.RequestOptions; +import org.signal.core.models.media.TransformProperties; import org.signal.core.util.concurrent.ListenableFuture; import org.signal.core.util.concurrent.SettableFuture; import org.signal.core.util.logging.Log; @@ -608,7 +609,14 @@ public class ThumbnailView extends FrameLayout { } private RequestBuilder buildThumbnailRequestBuilder(@NonNull RequestManager requestManager, @NonNull Slide slide) { - RequestBuilder requestBuilder = applySizing(requestManager.load(new DecryptableUri(Objects.requireNonNull(slide.getDisplayUri()))) + long videoTrimStartTimeUs = 0; + TransformProperties transformProperties = slide.asAttachment().transformProperties; + + if (transformProperties != null && !transformProperties.shouldSkipTransform()) { + videoTrimStartTimeUs = transformProperties.videoTrimStartTimeUs; + } + + RequestBuilder requestBuilder = applySizing(requestManager.load(new DecryptableUri(Objects.requireNonNull(slide.getDisplayUri()), videoTrimStartTimeUs)) .diskCacheStrategy(DiskCacheStrategy.RESOURCE) .downsample(SignalDownsampleStrategy.CENTER_OUTSIDE_NO_UPSCALE) .transition(withCrossFade())); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/items/V2ConversationItemThumbnail.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/items/V2ConversationItemThumbnail.kt index ada67d29b8..6bb925047a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/items/V2ConversationItemThumbnail.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/items/V2ConversationItemThumbnail.kt @@ -158,9 +158,16 @@ class V2ConversationItemThumbnail @JvmOverloads constructor( } if (thumbnailUri != null) { + val transformProperties = thumbnailAttachment.transformProperties + val videoTrimStartTimeUs = if (transformProperties != null && !transformProperties.skipTransform) { + transformProperties.videoTrimStartTimeUs + } else { + 0L + } + conversationContext .requestManager - .load(DecryptableUri(thumbnailUri)) + .load(DecryptableUri(thumbnailUri, videoTrimStartTimeUs)) .centerInside() .dontAnimate() .override(thumbnailSize.width, thumbnailSize.height) diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/SignalGlideDependenciesProvider.kt b/app/src/main/java/org/thoughtcrime/securesms/dependencies/SignalGlideDependenciesProvider.kt index 34859a1447..a4c5aa43ef 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/SignalGlideDependenciesProvider.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/SignalGlideDependenciesProvider.kt @@ -11,7 +11,7 @@ import org.signal.glide.common.io.InputStreamFactory import org.thoughtcrime.securesms.glide.DecryptableStreamFactory object SignalGlideDependenciesProvider : SignalGlideDependencies.Provider { - override fun getUriInputStreamFactory(uri: Uri): InputStreamFactory { - return DecryptableStreamFactory(uri) + override fun getUriInputStreamFactory(uri: Uri, thumbnailTimeUs: Long): InputStreamFactory { + return DecryptableStreamFactory(uri, thumbnailTimeUs) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/glide/DecryptableStreamFactory.kt b/app/src/main/java/org/thoughtcrime/securesms/glide/DecryptableStreamFactory.kt index c4e0a16112..9239abd29c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/glide/DecryptableStreamFactory.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/glide/DecryptableStreamFactory.kt @@ -15,7 +15,8 @@ import java.io.InputStream * A factory that creates a new [InputStream] for the given [Uri] each time [create] is called. */ class DecryptableStreamFactory( - private val uri: Uri + private val uri: Uri, + private val thumbnailTimeUs: Long = 0 ) : InputStreamFactory { companion object { private val TAG = Log.tag(DecryptableStreamFactory::class) @@ -23,7 +24,7 @@ class DecryptableStreamFactory( override fun create(): InputStream { return try { - DecryptableStreamLocalUriFetcher(AppDependencies.application, uri).loadResource(uri, AppDependencies.application.contentResolver) + DecryptableStreamLocalUriFetcher(AppDependencies.application, uri, thumbnailTimeUs).loadResource(uri, AppDependencies.application.contentResolver) } catch (e: Exception) { Log.w(TAG, "Error creating input stream for URI.", e) throw e diff --git a/app/src/main/java/org/thoughtcrime/securesms/glide/DecryptableStreamLocalUriFetcher.java b/app/src/main/java/org/thoughtcrime/securesms/glide/DecryptableStreamLocalUriFetcher.java index ce428614ef..9aad17ff52 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/glide/DecryptableStreamLocalUriFetcher.java +++ b/app/src/main/java/org/thoughtcrime/securesms/glide/DecryptableStreamLocalUriFetcher.java @@ -35,16 +35,19 @@ class DecryptableStreamLocalUriFetcher extends StreamLocalUriFetcher { private static final long TOTAL_PIXEL_SIZE_LIMIT = 200_000_000L; // 200 megapixels private final Context context; + private final long thumbnailTimeUs; - DecryptableStreamLocalUriFetcher(Context context, Uri uri) { + DecryptableStreamLocalUriFetcher(Context context, Uri uri, long thumbnailTimeUs) { super(context.getContentResolver(), uri); - this.context = context; + this.context = context; + this.thumbnailTimeUs = thumbnailTimeUs; } @Override protected InputStream loadResource(Uri uri, ContentResolver contentResolver) throws FileNotFoundException { if (MediaUtil.hasVideoThumbnail(context, uri)) { - Bitmap thumbnail = MediaUtil.getVideoThumbnail(context, uri, 1000); + long timeUs = thumbnailTimeUs > 0 ? thumbnailTimeUs : 1000; + Bitmap thumbnail = MediaUtil.getVideoThumbnail(context, uri, timeUs); if (thumbnail != null) { ByteArrayOutputStream baos = new ByteArrayOutputStream(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/MediaReviewFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/MediaReviewFragment.kt index 31f6f0aa96..2192deed01 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/MediaReviewFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/MediaReviewFragment.kt @@ -54,6 +54,7 @@ import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionNavigator import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionState import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionViewModel import org.thoughtcrime.securesms.mediasend.v2.stories.StoriesMultiselectForwardActivity +import org.thoughtcrime.securesms.mediasend.v2.videos.VideoTrimData import org.thoughtcrime.securesms.mms.MediaConstraints import org.thoughtcrime.securesms.mms.SentMediaQuality import org.thoughtcrime.securesms.recipients.Recipient @@ -339,7 +340,10 @@ class MediaReviewFragment : Fragment(R.layout.v2_media_review_fragment), Schedul pagerAdapter.submitMedia(state.selectedMedia) selectionAdapter.submitList( - state.selectedMedia.map { MediaReviewSelectedItem.Model(it, state.focusedMedia == it) } + MediaReviewAddItem.Model + state.selectedMedia.map { + val trimStartTimeUs = (state.editorStateMap[it.uri] as? VideoTrimData)?.startTimeUs ?: 0L + MediaReviewSelectedItem.Model(it, state.focusedMedia == it, trimStartTimeUs) + } + MediaReviewAddItem.Model ) presentSendButton(readyToSend, state.sendType, state.recipient) diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/MediaReviewSelectedItem.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/MediaReviewSelectedItem.kt index 8927f78d9b..bf61e38195 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/MediaReviewSelectedItem.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/MediaReviewSelectedItem.kt @@ -20,13 +20,13 @@ object MediaReviewSelectedItem { mappingAdapter.registerFactory(Model::class.java, LayoutFactory({ ViewHolder(it, onSelectedMediaClicked) }, R.layout.v2_media_review_selected_item)) } - class Model(val media: Media, val isSelected: Boolean) : MappingModel { + class Model(val media: Media, val isSelected: Boolean, val videoTrimStartTimeUs: Long = 0) : MappingModel { override fun areItemsTheSame(newItem: Model): Boolean { return media == newItem.media } override fun areContentsTheSame(newItem: Model): Boolean { - return media == newItem.media && isSelected == newItem.isSelected + return media == newItem.media && isSelected == newItem.isSelected && videoTrimStartTimeUs == newItem.videoTrimStartTimeUs } } @@ -38,7 +38,7 @@ object MediaReviewSelectedItem { override fun bind(model: Model) { Glide.with(imageView) - .load(DecryptableUri(model.media.uri)) + .load(DecryptableUri(model.media.uri, model.videoTrimStartTimeUs)) .centerCrop() .into(imageView) diff --git a/lib/glide/src/main/java/org/signal/glide/SignalGlideDependencies.kt b/lib/glide/src/main/java/org/signal/glide/SignalGlideDependencies.kt index 4926597b1f..ebc4b85356 100644 --- a/lib/glide/src/main/java/org/signal/glide/SignalGlideDependencies.kt +++ b/lib/glide/src/main/java/org/signal/glide/SignalGlideDependencies.kt @@ -29,12 +29,12 @@ object SignalGlideDependencies { val application: Application get() = _application - fun getUriInputStreamFactory(uri: Uri): InputStreamFactory = _provider.getUriInputStreamFactory(uri) + fun getUriInputStreamFactory(uri: Uri, thumbnailTimeUs: Long = 0): InputStreamFactory = _provider.getUriInputStreamFactory(uri, thumbnailTimeUs) interface Provider { /** - * A factory which can create an [java.io.InputStream] from a given [Uri] + * A factory which can create an [java.io.InputStream] from a given [Uri]. For videos, [thumbnailTimeUs] specifies the frame time to extract. */ - fun getUriInputStreamFactory(uri: Uri): InputStreamFactory + fun getUriInputStreamFactory(uri: Uri, thumbnailTimeUs: Long = 0): InputStreamFactory } } diff --git a/lib/glide/src/main/java/org/signal/glide/common/io/InputStreamFactory.kt b/lib/glide/src/main/java/org/signal/glide/common/io/InputStreamFactory.kt index 4b2ec9d0f9..d391bc3444 100644 --- a/lib/glide/src/main/java/org/signal/glide/common/io/InputStreamFactory.kt +++ b/lib/glide/src/main/java/org/signal/glide/common/io/InputStreamFactory.kt @@ -17,7 +17,8 @@ import java.io.InputStream interface InputStreamFactory { companion object { @JvmStatic - fun build(uri: Uri): InputStreamFactory = SignalGlideDependencies.getUriInputStreamFactory(uri) + @JvmOverloads + fun build(uri: Uri, thumbnailTimeUs: Long = 0): InputStreamFactory = SignalGlideDependencies.getUriInputStreamFactory(uri, thumbnailTimeUs) @JvmStatic fun build(file: File): InputStreamFactory = FileInputStreamFactory(file) diff --git a/lib/glide/src/main/java/org/signal/glide/decryptableuri/DecryptableUri.kt b/lib/glide/src/main/java/org/signal/glide/decryptableuri/DecryptableUri.kt index 86ce1f6c37..50ec24a857 100644 --- a/lib/glide/src/main/java/org/signal/glide/decryptableuri/DecryptableUri.kt +++ b/lib/glide/src/main/java/org/signal/glide/decryptableuri/DecryptableUri.kt @@ -9,8 +9,9 @@ import android.net.Uri import com.bumptech.glide.load.Key import java.security.MessageDigest -data class DecryptableUri(val uri: Uri) : Key { +data class DecryptableUri @JvmOverloads constructor(val uri: Uri, val thumbnailTimeUs: Long = 0) : Key { override fun updateDiskCacheKey(messageDigest: MessageDigest) { messageDigest.update(uri.toString().toByteArray()) + messageDigest.update(thumbnailTimeUs.toString().toByteArray()) } } diff --git a/lib/glide/src/main/java/org/signal/glide/decryptableuri/DecryptableUriStreamFetcher.kt b/lib/glide/src/main/java/org/signal/glide/decryptableuri/DecryptableUriStreamFetcher.kt index 31cfde83d6..21dc590fa9 100644 --- a/lib/glide/src/main/java/org/signal/glide/decryptableuri/DecryptableUriStreamFetcher.kt +++ b/lib/glide/src/main/java/org/signal/glide/decryptableuri/DecryptableUriStreamFetcher.kt @@ -22,7 +22,7 @@ class DecryptableUriStreamFetcher( override fun loadData(priority: Priority, callback: DataFetcher.DataCallback) { try { - callback.onDataReady(InputStreamFactory.build(decryptableUri.uri)) + callback.onDataReady(InputStreamFactory.build(decryptableUri.uri, decryptableUri.thumbnailTimeUs)) } catch (e: Exception) { callback.onLoadFailed(e) }