From f63ce79f1630fe9881c58e379af209ae2cc2ee32 Mon Sep 17 00:00:00 2001 From: Nicholas Date: Fri, 30 Sep 2022 09:42:06 -0400 Subject: [PATCH] Create new Media Preview infrastructure, behind feature flag. --- app/src/main/AndroidManifest.xml | 4 ++ .../securesms/MediaPreviewActivity.java | 51 ++++++-------- .../animation/DepthPageTransformer.java | 3 +- .../components/ThreadPhotoRailView.java | 2 +- .../conversation/ConversationItem.java | 29 ++++---- .../securesms/database/MediaDatabase.java | 2 +- .../loaders/GroupedThreadMediaLoader.java | 2 +- .../MediaOverviewPageFragment.java | 28 ++++---- .../mediapreview/MediaIntentFactory.kt | 66 +++++++++++++++++++ .../mediapreview/MediaPreviewRepository.kt | 65 ++++++++++++++++++ .../mediapreview/MediaPreviewV2Activity.kt | 28 ++++++++ .../mediapreview/MediaPreviewV2Fragment.kt | 57 ++++++++++++++++ .../mediapreview/MediaPreviewV2State.kt | 10 +++ .../mediapreview/MediaPreviewV2ViewModel.kt | 30 +++++++++ .../mediapreview/MediaPreviewViewModel.java | 6 +- .../mediapreview/PreviewMediaAdapter.kt | 39 +++++++++++ .../securesms/mms/AttachmentManager.java | 22 +++++-- .../securesms/util/FeatureFlags.java | 14 +++- .../res/layout/activity_mediapreview_v2.xml | 6 ++ .../res/layout/fragment_media_preview_v2.xml | 13 ++++ 20 files changed, 406 insertions(+), 71 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaIntentFactory.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewRepository.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2Activity.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2Fragment.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2State.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2ViewModel.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/mediapreview/PreviewMediaAdapter.kt create mode 100644 app/src/main/res/layout/activity_mediapreview_v2.xml create mode 100644 app/src/main/res/layout/fragment_media_preview_v2.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index eb7611d2c..55883783d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -686,6 +686,10 @@ android:screenOrientation="portrait" android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/> + + diff --git a/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java b/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java index 53fcd3836..ffb3c432d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java @@ -65,6 +65,7 @@ import org.thoughtcrime.securesms.database.MediaDatabase; import org.thoughtcrime.securesms.database.MediaDatabase.MediaRecord; import org.thoughtcrime.securesms.database.loaders.PagingMediaLoader; import org.thoughtcrime.securesms.mediaoverview.MediaOverviewActivity; +import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory; import org.thoughtcrime.securesms.mediapreview.MediaPreviewFragment; import org.thoughtcrime.securesms.mediapreview.MediaPreviewViewModel; import org.thoughtcrime.securesms.mediapreview.MediaRailAdapter; @@ -98,18 +99,6 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity private final static String TAG = Log.tag(MediaPreviewActivity.class); - private static final int NOT_IN_A_THREAD = -2; - - public static final String THREAD_ID_EXTRA = "thread_id"; - public static final String DATE_EXTRA = "date"; - public static final String SIZE_EXTRA = "size"; - public static final String CAPTION_EXTRA = "caption"; - public static final String LEFT_IS_RECENT_EXTRA = "left_is_recent"; - public static final String HIDE_ALL_MEDIA_EXTRA = "came_from_all_media"; - public static final String SHOW_THREAD_EXTRA = "show_thread"; - public static final String SORTING_EXTRA = "sorting"; - public static final String IS_VIDEO_GIF = "is_video_gif"; - private ViewPager mediaPager; private View detailsContainer; private TextView caption; @@ -127,7 +116,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity private ViewPagerListener viewPagerListener; private int restartItem = -1; - private long threadId = NOT_IN_A_THREAD; + private long threadId = MediaIntentFactory.NOT_IN_A_THREAD; private boolean cameFromAllMedia; private boolean showThread; private MediaDatabase.Sorting sorting; @@ -143,12 +132,12 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity { DatabaseAttachment attachment = Objects.requireNonNull(mediaRecord.getAttachment()); Intent intent = new Intent(context, MediaPreviewActivity.class); - intent.putExtra(MediaPreviewActivity.THREAD_ID_EXTRA, mediaRecord.getThreadId()); - intent.putExtra(MediaPreviewActivity.DATE_EXTRA, mediaRecord.getDate()); - intent.putExtra(MediaPreviewActivity.SIZE_EXTRA, attachment.getSize()); - intent.putExtra(MediaPreviewActivity.CAPTION_EXTRA, attachment.getCaption()); - intent.putExtra(MediaPreviewActivity.LEFT_IS_RECENT_EXTRA, leftIsRecent); - intent.putExtra(MediaPreviewActivity.IS_VIDEO_GIF, attachment.isVideoGif()); + intent.putExtra(MediaIntentFactory.THREAD_ID_EXTRA, mediaRecord.getThreadId()); + intent.putExtra(MediaIntentFactory.DATE_EXTRA, mediaRecord.getDate()); + intent.putExtra(MediaIntentFactory.SIZE_EXTRA, attachment.getSize()); + intent.putExtra(MediaIntentFactory.CAPTION_EXTRA, attachment.getCaption()); + intent.putExtra(MediaIntentFactory.LEFT_IS_RECENT_EXTRA, leftIsRecent); + intent.putExtra(MediaIntentFactory.IS_VIDEO_GIF, attachment.isVideoGif()); intent.setDataAndType(attachment.getUri(), mediaRecord.getContentType()); return intent; } @@ -305,17 +294,17 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity private void initializeResources() { Intent intent = getIntent(); - threadId = intent.getLongExtra(THREAD_ID_EXTRA, NOT_IN_A_THREAD); - cameFromAllMedia = intent.getBooleanExtra(HIDE_ALL_MEDIA_EXTRA, false); - showThread = intent.getBooleanExtra(SHOW_THREAD_EXTRA, false); - sorting = MediaDatabase.Sorting.values()[intent.getIntExtra(SORTING_EXTRA, 0)]; + threadId = intent.getLongExtra(MediaIntentFactory.THREAD_ID_EXTRA, MediaIntentFactory.NOT_IN_A_THREAD); + cameFromAllMedia = intent.getBooleanExtra(MediaIntentFactory.HIDE_ALL_MEDIA_EXTRA, false); + showThread = intent.getBooleanExtra(MediaIntentFactory.SHOW_THREAD_EXTRA, false); + sorting = MediaDatabase.Sorting.values()[intent.getIntExtra(MediaIntentFactory.SORTING_EXTRA, 0)]; initialMediaUri = intent.getData(); initialMediaType = intent.getType(); - initialMediaSize = intent.getLongExtra(SIZE_EXTRA, 0); - initialCaption = intent.getStringExtra(CAPTION_EXTRA); - leftIsRecent = intent.getBooleanExtra(LEFT_IS_RECENT_EXTRA, false); - initialMediaIsVideoGif = intent.getBooleanExtra(IS_VIDEO_GIF, false); + initialMediaSize = intent.getLongExtra(MediaIntentFactory.SIZE_EXTRA, 0); + initialCaption = intent.getStringExtra(MediaIntentFactory.CAPTION_EXTRA); + leftIsRecent = intent.getBooleanExtra(MediaIntentFactory.LEFT_IS_RECENT_EXTRA, false); + initialMediaIsVideoGif = intent.getBooleanExtra(MediaIntentFactory.IS_VIDEO_GIF, false); restartItem = -1; } @@ -533,7 +522,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity } private boolean isMediaInDb() { - return threadId != NOT_IN_A_THREAD; + return threadId != MediaIntentFactory.NOT_IN_A_THREAD; } private @Nullable MediaItem getCurrentMediaItem() { @@ -789,7 +778,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity cursor.moveToPosition(cursorPosition); - MediaDatabase.MediaRecord mediaRecord = MediaDatabase.MediaRecord.from(context, cursor); + MediaDatabase.MediaRecord mediaRecord = MediaDatabase.MediaRecord.from(cursor); DatabaseAttachment attachment = Objects.requireNonNull(mediaRecord.getAttachment()); MediaPreviewFragment fragment = MediaPreviewFragment.newInstance(attachment, autoPlay); @@ -819,7 +808,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity cursor.moveToPosition(cursorPosition); - MediaRecord mediaRecord = MediaRecord.from(context, cursor); + MediaRecord mediaRecord = MediaRecord.from(cursor); DatabaseAttachment attachment = Objects.requireNonNull(mediaRecord.getAttachment()); RecipientId recipientId = mediaRecord.getRecipientId(); RecipientId threadRecipientId = mediaRecord.getThreadRecipientId(); @@ -890,4 +879,4 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity @Nullable View getPlaybackControls(int position); boolean hasFragmentFor(int position); } -} +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/animation/DepthPageTransformer.java b/app/src/main/java/org/thoughtcrime/securesms/animation/DepthPageTransformer.java index da935281b..85ca770bf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/animation/DepthPageTransformer.java +++ b/app/src/main/java/org/thoughtcrime/securesms/animation/DepthPageTransformer.java @@ -4,11 +4,12 @@ import android.view.View; import androidx.annotation.NonNull; import androidx.viewpager.widget.ViewPager; +import androidx.viewpager2.widget.ViewPager2; /** * Based on https://developer.android.com/training/animation/screen-slide#depth-page */ -public final class DepthPageTransformer implements ViewPager.PageTransformer { +public final class DepthPageTransformer implements ViewPager.PageTransformer, ViewPager2.PageTransformer { private static final float MIN_SCALE = 0.75f; public void transformPage(@NonNull View view, float position) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/ThreadPhotoRailView.java b/app/src/main/java/org/thoughtcrime/securesms/components/ThreadPhotoRailView.java index 05d1b56b5..4c09af37e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/ThreadPhotoRailView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/ThreadPhotoRailView.java @@ -89,7 +89,7 @@ public class ThreadPhotoRailView extends FrameLayout { @Override public void onBindItemViewHolder(ThreadPhotoViewHolder viewHolder, @NonNull Cursor cursor) { ThumbnailView imageView = viewHolder.imageView; - MediaDatabase.MediaRecord mediaRecord = MediaDatabase.MediaRecord.from(getContext(), cursor); + MediaDatabase.MediaRecord mediaRecord = MediaDatabase.MediaRecord.from(cursor); Slide slide = MediaUtil.getSlideForAttachment(getContext(), mediaRecord.getAttachment()); if (slide != null) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java index 66e2dd01f..7272b35f6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java @@ -18,6 +18,7 @@ package org.thoughtcrime.securesms.conversation; import android.animation.ValueAnimator; import android.annotation.SuppressLint; +import android.app.Activity; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; @@ -96,6 +97,7 @@ import org.thoughtcrime.securesms.conversation.colors.Colorizer; import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectCollection; import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart; import org.thoughtcrime.securesms.database.AttachmentDatabase; +import org.thoughtcrime.securesms.database.MediaDatabase; import org.thoughtcrime.securesms.database.MessageDatabase; import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord; @@ -111,6 +113,8 @@ import org.thoughtcrime.securesms.jobs.MmsSendJob; import org.thoughtcrime.securesms.jobs.SmsSendJob; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.linkpreview.LinkPreview; +import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory; +import org.thoughtcrime.securesms.mediapreview.MediaPreviewV2Activity; import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.mms.ImageSlide; import org.thoughtcrime.securesms.mms.PartAuthority; @@ -126,6 +130,7 @@ import org.thoughtcrime.securesms.recipients.RecipientForeverObserver; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.revealable.ViewOnceMessageView; import org.thoughtcrime.securesms.util.DateUtils; +import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.InterceptableLongClickCopyLinkSpan; import org.thoughtcrime.securesms.util.LinkUtil; import org.thoughtcrime.securesms.util.LongClickMovementMethod; @@ -2312,17 +2317,19 @@ public final class ConversationItem extends RelativeLayout implements BindableCo } else if (!canPlayContent && mediaItem != null && eventListener != null) { eventListener.onPlayInlineContent(conversationMessage); } else if (MediaPreviewActivity.isContentTypeSupported(slide.getContentType()) && slide.getUri() != null) { - Intent intent = new Intent(context, MediaPreviewActivity.class); - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - intent.setDataAndType(slide.getUri(), slide.getContentType()); - intent.putExtra(MediaPreviewActivity.THREAD_ID_EXTRA, messageRecord.getThreadId()); - intent.putExtra(MediaPreviewActivity.DATE_EXTRA, messageRecord.getTimestamp()); - intent.putExtra(MediaPreviewActivity.SIZE_EXTRA, slide.asAttachment().getSize()); - intent.putExtra(MediaPreviewActivity.CAPTION_EXTRA, slide.getCaption().orElse(null)); - intent.putExtra(MediaPreviewActivity.IS_VIDEO_GIF, slide.isVideoGif()); - intent.putExtra(MediaPreviewActivity.LEFT_IS_RECENT_EXTRA, false); - - context.startActivity(intent); + MediaIntentFactory.MediaPreviewArgs args = new MediaIntentFactory.MediaPreviewArgs( + messageRecord.getThreadId(), + messageRecord.getTimestamp(), + slide.getUri(), + slide.getContentType(), + slide.asAttachment().getSize(), + slide.getCaption().orElse(null), + false, + false, + false, + MediaDatabase.Sorting.Newest.ordinal(), + slide.isVideoGif()); + context.startActivity(MediaIntentFactory.create(context, args)); } else if (slide.getUri() != null) { Log.i(TAG, "Clicked: " + slide.getUri() + " , " + slide.getContentType()); Uri publicUri = PartAuthority.getAttachmentPublicUri(slide.getUri()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MediaDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MediaDatabase.java index 7a4fd1463..ea5313c3b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MediaDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MediaDatabase.java @@ -192,7 +192,7 @@ public class MediaDatabase extends Database { this.outgoing = outgoing; } - public static MediaRecord from(@NonNull Context context, @NonNull Cursor cursor) { + public static MediaRecord from(@NonNull Cursor cursor) { AttachmentDatabase attachmentDatabase = SignalDatabase.attachments(); List attachments = attachmentDatabase.getAttachments(cursor); RecipientId recipientId = RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.RECIPIENT_ID))); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/loaders/GroupedThreadMediaLoader.java b/app/src/main/java/org/thoughtcrime/securesms/database/loaders/GroupedThreadMediaLoader.java index 26da50d08..8b2169ea8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/loaders/GroupedThreadMediaLoader.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/loaders/GroupedThreadMediaLoader.java @@ -75,7 +75,7 @@ public final class GroupedThreadMediaLoader extends AsyncTaskLoader> { + return Single.fromCallable { + val cursor = media.getGalleryMediaForThread(threadId, sorting) + + val acc = mutableListOf() + var attachmentUri: Uri? = null + while (cursor.moveToNext()) { + val attachmentId = AttachmentId(cursor.requireLong(AttachmentDatabase.ROW_ID), cursor.requireLong(AttachmentDatabase.UNIQUE_ID)) + attachmentUri = PartAuthority.getAttachmentDataUri(attachmentId) + if (attachmentUri == startingUri) { + break + } + } + + if (attachmentUri == startingUri) { + for (i in 0..limit) { + val element = MediaDatabase.MediaRecord.from(cursor).attachment + if (element != null) { + acc.add(element) + } + if (!cursor.isLast) { + cursor.moveToNext() + } else { + break + } + } + acc.toList() + } else { + Log.e(TAG, "Could not find $startingUri in thread $threadId") + emptyList() + } + }.subscribeOn(Schedulers.io()).toFlowable() + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2Activity.kt b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2Activity.kt new file mode 100644 index 000000000..3735f8fc0 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2Activity.kt @@ -0,0 +1,28 @@ +package org.thoughtcrime.securesms.mediapreview + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.commit +import org.thoughtcrime.securesms.R + +class MediaPreviewV2Activity : AppCompatActivity(R.layout.activity_mediapreview_v2) { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + if (savedInstanceState == null) { + val bundle = Bundle() + val args = MediaIntentFactory.requireArguments(intent.extras!!) + bundle.putParcelable(MediaPreviewV2Fragment.ARGS_KEY, args) + supportFragmentManager.commit { + setReorderingAllowed(true) + add(R.id.fragment_container_view, MediaPreviewV2Fragment::class.java, bundle, FRAGMENT_TAG) + } + } + } + + companion object { + private const val FRAGMENT_TAG = "media_preview_fragment_v2" + private const val NOT_IN_A_THREAD = -2 + + const val THREAD_ID_EXTRA = "thread_id" + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2Fragment.kt b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2Fragment.kt new file mode 100644 index 000000000..3a22843c4 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2Fragment.kt @@ -0,0 +1,57 @@ +package org.thoughtcrime.securesms.mediapreview + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import org.signal.core.util.logging.Log +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.animation.DepthPageTransformer +import org.thoughtcrime.securesms.components.ViewBinderDelegate +import org.thoughtcrime.securesms.database.MediaDatabase +import org.thoughtcrime.securesms.databinding.FragmentMediaPreviewV2Binding +import org.thoughtcrime.securesms.util.LifecycleDisposable + +class MediaPreviewV2Fragment : Fragment(R.layout.fragment_media_preview_v2), MediaPreviewFragment.Events { + private val TAG = Log.tag(MediaPreviewV2Fragment::class.java) + + private val lifecycleDisposable = LifecycleDisposable() + private val binding by ViewBinderDelegate(FragmentMediaPreviewV2Binding::bind) + private val viewModel: MediaPreviewV2ViewModel by viewModels() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.mediaPager.offscreenPageLimit = 1 + binding.mediaPager.setPageTransformer(DepthPageTransformer()) + lifecycleDisposable += viewModel.state.distinctUntilChanged().observeOn(AndroidSchedulers.mainThread()).subscribe { + if (it.loadState == MediaPreviewV2State.LoadState.READY) { + binding.mediaPager.adapter = PreviewMediaAdapter(this, it.attachments) + } + } + initializeViewModel() + } + + private fun initializeViewModel() { + val args = MediaIntentFactory.requireArguments(requireArguments()) + val sorting = MediaDatabase.Sorting.values()[args.sorting] + viewModel.fetchAttachments(args.initialMediaUri, args.threadId, sorting) + } + + override fun singleTapOnMedia(): Boolean { + Log.d(TAG, "singleTapOnMedia()") + return true + } + + override fun mediaNotAvailable() { + Log.d(TAG, "mediaNotAvailable()") + } + + override fun onMediaReady() { + Log.d(TAG, "onMediaReady()") + } + + companion object { + val ARGS_KEY: String = "args" + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2State.kt b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2State.kt new file mode 100644 index 000000000..ef873fc32 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2State.kt @@ -0,0 +1,10 @@ +package org.thoughtcrime.securesms.mediapreview + +import org.thoughtcrime.securesms.attachments.Attachment + +data class MediaPreviewV2State( + val attachments: List = emptyList(), + val loadState: LoadState = LoadState.INIT +) { + enum class LoadState { INIT, READY, } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2ViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2ViewModel.kt new file mode 100644 index 000000000..6de2873d9 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2ViewModel.kt @@ -0,0 +1,30 @@ +package org.thoughtcrime.securesms.mediapreview + +import android.net.Uri +import androidx.lifecycle.ViewModel +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.core.Flowable +import io.reactivex.rxjava3.disposables.CompositeDisposable +import io.reactivex.rxjava3.kotlin.plusAssign +import org.signal.core.util.logging.Log +import org.thoughtcrime.securesms.database.MediaDatabase +import org.thoughtcrime.securesms.util.rx.RxStore + +class MediaPreviewV2ViewModel : ViewModel() { + private val TAG = Log.tag(MediaPreviewV2ViewModel::class.java) + private val store = RxStore(MediaPreviewV2State()) + private val disposables = CompositeDisposable() + private val repository: MediaPreviewRepository = MediaPreviewRepository() + + val state: Flowable = store.stateFlowable.observeOn(AndroidSchedulers.mainThread()) + + fun fetchAttachments(startingUri: Uri, threadId: Long, sorting: MediaDatabase.Sorting) { + disposables += store.update(repository.getAttachments(startingUri, threadId, sorting)) { attachments, oldState -> + oldState.copy(attachments = attachments, loadState = MediaPreviewV2State.LoadState.READY) + } + } + + override fun onCleared() { + disposables.dispose() + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewViewModel.java index 2ec38b000..70a986365 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewViewModel.java @@ -47,14 +47,14 @@ public class MediaPreviewViewModel extends ViewModel { cursor.moveToPosition(activePosition); - MediaRecord activeRecord = MediaRecord.from(context, cursor); + MediaRecord activeRecord = MediaRecord.from(cursor); LinkedList rail = new LinkedList<>(); Media activeMedia = toMedia(activeRecord); if (activeMedia != null) rail.add(activeMedia); while (cursor.moveToPrevious()) { - MediaRecord record = MediaRecord.from(context, cursor); + MediaRecord record = MediaRecord.from(cursor); if (record.getAttachment().getMmsId() == activeRecord.getAttachment().getMmsId()) { Media media = toMedia(record); if (media != null) rail.addFirst(media); @@ -66,7 +66,7 @@ public class MediaPreviewViewModel extends ViewModel { cursor.moveToPosition(activePosition); while (cursor.moveToNext()) { - MediaRecord record = MediaRecord.from(context, cursor); + MediaRecord record = MediaRecord.from(cursor); if (record.getAttachment().getMmsId() == activeRecord.getAttachment().getMmsId()) { Media media = toMedia(record); if (media != null) rail.addLast(media); diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/PreviewMediaAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/PreviewMediaAdapter.kt new file mode 100644 index 000000000..781a46153 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/PreviewMediaAdapter.kt @@ -0,0 +1,39 @@ +package org.thoughtcrime.securesms.mediapreview + +import androidx.core.os.bundleOf +import androidx.fragment.app.Fragment +import androidx.viewpager2.adapter.FragmentStateAdapter +import org.thoughtcrime.securesms.attachments.Attachment +import org.thoughtcrime.securesms.util.MediaUtil + +class PreviewMediaAdapter(val fragment: Fragment, val items: List) : FragmentStateAdapter(fragment) { + var autoPlayPosition = -1 + + override fun getItemCount(): Int { + return items.count() + } + + override fun createFragment(position: Int): Fragment { + val attachment: Attachment = items[position] + + val contentType = attachment.contentType + val args = bundleOf( + MediaPreviewFragment.DATA_URI to attachment.uri, + MediaPreviewFragment.DATA_CONTENT_TYPE to contentType, + MediaPreviewFragment.DATA_SIZE to attachment.size, + MediaPreviewFragment.AUTO_PLAY to (position == autoPlayPosition), + MediaPreviewFragment.VIDEO_GIF to attachment.isVideoGif, + ) + val fragment = if (MediaUtil.isVideo(contentType)) { + VideoMediaPreviewFragment() + } else if (MediaUtil.isImageType(contentType)) { + ImageMediaPreviewFragment() + } else { + throw AssertionError("Unexpected media type: $contentType") + } + + fragment.arguments = args + + return fragment + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/AttachmentManager.java b/app/src/main/java/org/thoughtcrime/securesms/mms/AttachmentManager.java index 61caa9e94..2e7adbea0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/AttachmentManager.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/AttachmentManager.java @@ -52,8 +52,10 @@ import org.thoughtcrime.securesms.components.location.SignalMapView; import org.thoughtcrime.securesms.components.location.SignalPlace; import org.thoughtcrime.securesms.conversation.MessageSendType; import org.thoughtcrime.securesms.database.AttachmentDatabase; +import org.thoughtcrime.securesms.database.MediaDatabase; import org.thoughtcrime.securesms.giph.ui.GiphyActivity; import org.thoughtcrime.securesms.maps.PlacePickerActivity; +import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory; import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionActivity; import org.thoughtcrime.securesms.payments.create.CreatePaymentFragmentArgs; import org.thoughtcrime.securesms.payments.preferences.PaymentsActivity; @@ -469,13 +471,19 @@ public class AttachmentManager { private void previewImageDraft(final @NonNull Slide slide) { if (MediaPreviewActivity.isContentTypeSupported(slide.getContentType()) && slide.getUri() != null) { - Intent intent = new Intent(context, MediaPreviewActivity.class); - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - intent.putExtra(MediaPreviewActivity.SIZE_EXTRA, slide.asAttachment().getSize()); - intent.putExtra(MediaPreviewActivity.CAPTION_EXTRA, slide.getCaption().orElse(null)); - intent.setDataAndType(slide.getUri(), slide.getContentType()); - - context.startActivity(intent); + MediaIntentFactory.MediaPreviewArgs args = new MediaIntentFactory.MediaPreviewArgs( + MediaIntentFactory.NOT_IN_A_THREAD, + MediaIntentFactory.UNKNOWN_TIMESTAMP, + slide.getUri(), + slide.getContentType(), + slide.asAttachment().getSize(), + slide.getCaption().orElse(null), + false, + false, + false, + MediaDatabase.Sorting.Newest.ordinal(), + slide.isVideoGif()); + context.startActivity(MediaIntentFactory.create(context, args)); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java index 107b917ae..431e127e0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java @@ -103,6 +103,7 @@ public final class FeatureFlags { private static final String CDS_V2_COMPAT = "android.cdsV2Compat.4"; public static final String STORIES_LOCALE = "android.stories.locale"; private static final String HIDE_CONTACTS = "android.hide.contacts"; + public static final String MEDIA_PREVIEW_V2 = "android.mediaPreviewV2"; /** * We will only store remote values for flags in this set. If you want a flag to be controllable @@ -157,7 +158,8 @@ public final class FeatureFlags { SMS_EXPORTER, CDS_V2_COMPAT, STORIES_LOCALE, - HIDE_CONTACTS + HIDE_CONTACTS, + MEDIA_PREVIEW_V2 ); @VisibleForTesting @@ -220,7 +222,8 @@ public final class FeatureFlags { RECIPIENT_MERGE_V2, CDS_V2_LOAD_TEST, CDS_V2_COMPAT, - STORIES + STORIES, + MEDIA_PREVIEW_V2 ); /** @@ -565,6 +568,13 @@ public final class FeatureFlags { return getBoolean(HIDE_CONTACTS, false); } + /** + * Whether or not we should use the new media preview fragment implementation. + */ + public static boolean mediaPreviewV2() { + return getBoolean(MEDIA_PREVIEW_V2, false); + } + /** Only for rendering debug info. */ public static synchronized @NonNull Map getMemoryValues() { return new TreeMap<>(REMOTE_VALUES); diff --git a/app/src/main/res/layout/activity_mediapreview_v2.xml b/app/src/main/res/layout/activity_mediapreview_v2.xml new file mode 100644 index 000000000..b6ed0ae4a --- /dev/null +++ b/app/src/main/res/layout/activity_mediapreview_v2.xml @@ -0,0 +1,6 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_media_preview_v2.xml b/app/src/main/res/layout/fragment_media_preview_v2.xml new file mode 100644 index 000000000..02f8c6def --- /dev/null +++ b/app/src/main/res/layout/fragment_media_preview_v2.xml @@ -0,0 +1,13 @@ + + + + + \ No newline at end of file