Refactor viewer to prepare for enhanced video duration support.

fork-5.53.8
Alex Hart 2022-02-24 17:39:56 -04:00
rodzic 9a5fcdbe4d
commit bd58c91d2c
13 zmienionych plików z 208 dodań i 60 usunięć

Wyświetl plik

@ -101,5 +101,8 @@ public abstract class MediaPreviewFragment extends Fragment {
public interface Events {
boolean singleTapOnMedia();
void mediaNotAvailable();
default @Nullable VideoControlsDelegate getVideoControlsDelegate() {
return null;
}
}
}

Wyświetl plik

@ -0,0 +1,71 @@
package org.thoughtcrime.securesms.mediapreview
import android.net.Uri
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.subjects.BehaviorSubject
import io.reactivex.rxjava3.subjects.PublishSubject
import org.thoughtcrime.securesms.video.VideoPlayer
/**
* Class to manage video playback in preview screen.
*/
class VideoControlsDelegate {
private val playWhenReady: MutableMap<Uri, Boolean> = mutableMapOf()
private val playerSubject = BehaviorSubject.create<Player>()
private val playerReadySignal = PublishSubject.create<Unit>()
val playerUpdates: Observable<PlayerUpdate> = playerReadySignal
.observeOn(AndroidSchedulers.mainThread())
.flatMap { playerSubject }
.filter { it.videoPlayer != null }
.map { PlayerUpdate(it.uri, it.videoPlayer?.duration!!) }
fun pause() = playerSubject.value?.videoPlayer?.pause()
fun resume(uri: Uri) {
val player = playerSubject.value
if (player?.uri == uri) {
player.videoPlayer?.play()
} else {
playWhenReady[uri] = true
}
playerSubject.value?.videoPlayer?.play()
}
fun restart() {
playerSubject.value?.videoPlayer?.playbackPosition = 0L
}
fun attachPlayer(uri: Uri, videoPlayer: VideoPlayer?) {
playerSubject.onNext(Player(uri, videoPlayer))
if ((videoPlayer?.duration ?: -1L) > 0L) {
playerReadySignal.onNext(Unit)
} else {
videoPlayer?.setPlayerStateCallbacks {
playerReadySignal.onNext(Unit)
}
}
if (playWhenReady[uri] == true) {
playWhenReady[uri] = false
videoPlayer?.play()
}
}
fun detachPlayer() {
playerSubject.onNext(Player())
}
private data class Player(
val uri: Uri = Uri.EMPTY,
val videoPlayer: VideoPlayer? = null
)
data class PlayerUpdate(
val mediaUri: Uri,
val duration: Long
)
}

Wyświetl plik

@ -6,6 +6,7 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.core.util.logging.Log;
@ -28,7 +29,8 @@ public final class VideoMediaPreviewFragment extends MediaPreviewFragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
Bundle savedInstanceState)
{
View itemView = inflater.inflate(R.layout.media_preview_video_fragment, container, false);
Bundle arguments = requireArguments();
Uri uri = arguments.getParcelable(DATA_URI);
@ -70,6 +72,10 @@ public final class VideoMediaPreviewFragment extends MediaPreviewFragment {
if (videoView != null && isVideoGif) {
videoView.play();
}
if (events.getVideoControlsDelegate() != null) {
events.getVideoControlsDelegate().attachPlayer(getUri(), videoView);
}
}
@Override
@ -77,10 +83,18 @@ public final class VideoMediaPreviewFragment extends MediaPreviewFragment {
if (videoView != null) {
videoView.pause();
}
if (events.getVideoControlsDelegate() != null) {
events.getVideoControlsDelegate().detachPlayer();
}
}
@Override
public View getPlaybackControls() {
return videoView != null && !isVideoGif ? videoView.getControlView() : null;
}
private @NonNull Uri getUri() {
return requireArguments().getParcelable(DATA_URI);
}
}

Wyświetl plik

@ -15,7 +15,6 @@ class StoryPost(
val viewCount: Int,
val replyCount: Int,
val dateInMilliseconds: Long,
val durationMillis: Long,
val attachment: Attachment,
val conversationMessage: ConversationMessage
)

Wyświetl plik

@ -33,6 +33,7 @@ import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectFor
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragmentArgs
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
import org.thoughtcrime.securesms.mediapreview.MediaPreviewFragment
import org.thoughtcrime.securesms.mediapreview.VideoControlsDelegate
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.stories.dialogs.StoryContextMenu
@ -48,6 +49,7 @@ import org.thoughtcrime.securesms.util.fragments.requireListener
import org.thoughtcrime.securesms.util.views.TouchInterceptingFrameLayout
import org.thoughtcrime.securesms.util.visible
import java.util.Locale
import java.util.concurrent.TimeUnit
import kotlin.math.abs
class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page), MediaPreviewFragment.Events, MultiselectForwardBottomSheet.Callback {
@ -65,6 +67,8 @@ class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page),
}
)
private val videoControlsDelegate = VideoControlsDelegate()
private val lifecycleDisposable = LifecycleDisposable()
private val storyRecipientId: RecipientId
@ -122,10 +126,10 @@ class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page),
cardWrapper.setOnTouchListener { _, event ->
val result = gestureDetector.onTouchEvent(event)
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
progressBar.pause()
viewModel.setIsUserTouching(true)
hideChrome()
} else if (event.actionMasked == MotionEvent.ACTION_UP || event.actionMasked == MotionEvent.ACTION_CANCEL) {
resumeProgressIfNotDisplayingDialog()
viewModel.setIsUserTouching(false)
showChrome()
}
@ -147,6 +151,10 @@ class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page),
.replace(R.id.story_content_container, createFragmentForPost(viewModel.getPostAt(newPageIndex)))
.commit()
}
if (oldPageIndex == newPageIndex) {
videoControlsDelegate.restart()
}
}
override fun onFinished() {
@ -166,27 +174,45 @@ class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page),
presentDistributionList(distributionList, post)
presentCaption(caption, largeCaption, largeCaptionOverlay, post)
if (progressBar.segmentCount != state.posts.size) {
val durations: Map<Int, Long> = state.posts
.mapIndexed { index, storyPost ->
index to (storyPost.attachment.uri?.let { state.durations[it] } ?: TimeUnit.SECONDS.toMillis(5))
}
.toMap()
if (progressBar.segmentCount != state.posts.size || progressBar.segmentDurations != durations) {
progressBar.segmentCount = state.posts.size
progressBar.segmentDurations = state.posts.mapIndexed { index, storyPost -> index to storyPost.durationMillis }.toMap()
progressBar.start()
progressBar.segmentDurations = durations
}
viewModel.setAreSegmentsInitialized(true)
} else if (state.selectedPostIndex >= state.posts.size) {
callback.onFinishedPosts(storyRecipientId)
}
}
viewModel.storyViewerPlaybackState.observe(viewLifecycleOwner) { state ->
if (state.isPaused) {
pauseProgress()
} else {
resumeProgress()
}
}
lifecycleDisposable.bindTo(viewLifecycleOwner)
lifecycleDisposable += viewModel.groupDirectReplyObservable.subscribe { opt ->
if (opt.isPresent) {
progressBar.pause()
when (val sheet = opt.get()) {
is StoryViewerDialog.GroupDirectReply -> {
onStartDirectReply(sheet.storyId, sheet.recipientId)
}
}
} else {
resumeProgress()
}
}
lifecycleDisposable += videoControlsDelegate.playerUpdates.subscribe { update ->
if (update.duration > 0L) {
viewModel.setDuration(update.mediaUri, update.duration)
}
}
@ -195,7 +221,6 @@ class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page),
override fun onPause() {
super.onPause()
progressBar.pause()
}
override fun onResume() {
@ -205,14 +230,12 @@ class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page),
progressBar.reset()
progressBar.setPosition(viewModel.getRestartIndex())
}
resumeProgressIfNotDisplayingDialog()
}
override fun onFinishForwardAction() = Unit
override fun onDismissForwardSheet() {
viewModel.onForwardDismissed()
viewModel.setIsDisplayingForwardDialog(false)
}
private fun hideChrome() {
@ -264,18 +287,18 @@ class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page),
constraintSet.applyTo(requireView() as ConstraintLayout)
}
private fun resumeProgressIfNotDisplayingDialog() {
if (childFragmentManager.findFragmentByTag(BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG) == null) {
resumeProgress()
}
}
private fun resumeProgress() {
if (progressBar.segmentCount != 0) {
progressBar.start()
videoControlsDelegate.resume(viewModel.getPost().attachment.uri!!)
}
}
private fun pauseProgress() {
progressBar.pause()
videoControlsDelegate.pause()
}
private fun startReply() {
val replyFragment: DialogFragment = when (viewModel.getSwipeToReplyState()) {
StoryViewerPageState.ReplyState.NONE -> return
@ -285,12 +308,17 @@ class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page),
StoryViewerPageState.ReplyState.GROUP_SELF -> StoryViewsAndRepliesDialogFragment.create(viewModel.getPost().id, viewModel.getPost().group!!.id, getViewsAndRepliesDialogStartPage())
}
progressBar.pause()
if (viewModel.getSwipeToReplyState() == StoryViewerPageState.ReplyState.PRIVATE) {
viewModel.setIsDisplayingDirectReplyDialog(true)
} else {
viewModel.setIsDisplayingViewsAndRepliesDialog(true)
}
replyFragment.showNow(childFragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
}
private fun onStartDirectReply(storyId: Long, recipientId: RecipientId) {
progressBar.pause()
viewModel.setIsDisplayingDirectReplyDialog(true)
StoryDirectReplyDialogFragment.create(
storyId = storyId,
recipientId = recipientId
@ -359,7 +387,7 @@ class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page),
largeCaptionOverlay.setOnClickListener {
onHideCaptionOverlay(caption, largeCaption, largeCaptionOverlay)
}
progressBar.pause()
viewModel.setIsDisplayingCaptionOverlay(true)
}
private fun onHideCaptionOverlay(caption: TextView, largeCaption: TextView, largeCaptionOverlay: View) {
@ -367,7 +395,7 @@ class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page),
largeCaption.visible = false
largeCaptionOverlay.visible = false
largeCaptionOverlay.setOnClickListener(null)
resumeProgress()
viewModel.setIsDisplayingCaptionOverlay(false)
}
private fun presentFrom(from: TextView, storyPost: StoryPost) {
@ -420,20 +448,24 @@ class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page),
}
private fun createFragmentForPost(storyPost: StoryPost): Fragment {
return MediaPreviewFragment.newInstance(storyPost.attachment, true)
return MediaPreviewFragment.newInstance(storyPost.attachment, false)
}
override fun getVideoControlsDelegate(): VideoControlsDelegate {
return videoControlsDelegate
}
private fun displayMoreContextMenu(anchor: View) {
progressBar.pause()
viewModel.setIsDisplayingContextMenu(true)
StoryContextMenu.show(
context = requireContext(),
anchorView = anchor,
storyViewerPageState = viewModel.getStateSnapshot(),
onDismiss = {
viewModel.onDismissContextMenu()
viewModel.setIsDisplayingContextMenu(false)
},
onForward = { storyPost ->
viewModel.startForward()
viewModel.setIsDisplayingForwardDialog(true)
MultiselectForwardFragmentArgs.create(
requireContext(),
storyPost.conversationMessage.multiselectCollection.toSet(),
@ -456,9 +488,9 @@ class StoryViewerPageFragment : Fragment(R.layout.stories_viewer_fragment_page),
StoryContextMenu.save(requireContext(), it.conversationMessage.messageRecord)
},
onDelete = {
viewModel.startDelete()
viewModel.setIsDisplayingDeleteDialog(true)
lifecycleDisposable += StoryContextMenu.delete(requireContext(), setOf(it.conversationMessage.messageRecord)).subscribe { _ ->
viewModel.onDeleteDismissed()
viewModel.setIsDisplayingDeleteDialog(false)
viewModel.refresh()
}
}

Wyświetl plik

@ -66,8 +66,7 @@ class StoryViewerPageRepository(context: Context) {
viewCount = record.viewedReceiptCount,
replyCount = SignalDatabase.mms.getNumberOfStoryReplies(record.id),
dateInMilliseconds = record.dateSent,
durationMillis = getDurationMillis(record as MmsMessageRecord),
attachment = record.slideDeck.firstSlide!!.asAttachment(),
attachment = (record as MmsMessageRecord).slideDeck.firstSlide!!.asAttachment(),
conversationMessage = ConversationMessage.ConversationMessageFactory.createWithUnresolvedData(context, record)
)
@ -125,16 +124,6 @@ class StoryViewerPageRepository(context: Context) {
}.observeOn(Schedulers.io())
}
private fun getDurationMillis(record: MmsMessageRecord): Long {
val slide = record.slideDeck.firstSlide!!
return if (slide.hasVideo()) {
// TODO [stories] Remove duration from this stuff... Videos will need to actually start playback before we know how long they are...
5000
} else {
5000
}
}
fun hideStory(recipientId: RecipientId): Completable {
return Completable.fromAction {
SignalDatabase.recipients.setHideStory(recipientId, true)

Wyświetl plik

@ -1,7 +1,10 @@
package org.thoughtcrime.securesms.stories.viewer.page
import android.net.Uri
data class StoryViewerPageState(
val posts: List<StoryPost> = emptyList(),
val durations: Map<Uri, Long> = emptyMap(),
val selectedPostIndex: Int = 0,
val replyState: ReplyState = ReplyState.NONE
) {

Wyświetl plik

@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.stories.viewer.page
import android.net.Uri
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
@ -28,6 +29,10 @@ class StoryViewerPageViewModel(
private val storyViewerDialogSubject: Subject<Optional<StoryViewerDialog>> = BehaviorSubject.createDefault(Optional.empty())
private val dismissSubject = PublishSubject.create<StoryViewerDialog.Type>()
private val storyViewerPlaybackStore = Store(StoryViewerPlaybackState())
val storyViewerPlaybackState: LiveData<StoryViewerPlaybackState> = storyViewerPlaybackStore.stateLiveData
val groupDirectReplyObservable: Observable<Optional<StoryViewerDialog>> = Observable.combineLatest(storyViewerDialogSubject, dismissSubject) { sheet, dismissed ->
if (sheet.isPresent && sheet.get().type != dismissed) {
sheet
@ -50,6 +55,12 @@ class StoryViewerPageViewModel(
refresh()
}
fun setDuration(uri: Uri, duration: Long) {
store.update {
it.copy(durations = it.durations + (uri to duration))
}
}
fun refresh() {
disposables.clear()
disposables += repository.getStoryPostsFor(recipientId).subscribe { posts ->
@ -95,32 +106,36 @@ class StoryViewerPageViewModel(
storyViewerDialogSubject.onNext(Optional.of(StoryViewerDialog.GroupDirectReply(recipientId, storyId)))
}
fun startForward() {
storyViewerDialogSubject.onNext(Optional.of(StoryViewerDialog.Forward))
fun setIsDisplayingContextMenu(isDisplayingContextMenu: Boolean) {
storyViewerPlaybackStore.update { it.copy(isDisplayingContextMenu = isDisplayingContextMenu) }
}
fun startDelete() {
storyViewerDialogSubject.onNext(Optional.of(StoryViewerDialog.Delete))
fun setIsDisplayingForwardDialog(isDisplayingForwardDialog: Boolean) {
storyViewerPlaybackStore.update { it.copy(isDisplayingForwardDialog = isDisplayingForwardDialog) }
}
fun onForwardDismissed() {
dismissSubject.onNext(StoryViewerDialog.Type.FORWARD)
fun setIsDisplayingDeleteDialog(isDisplayingDeleteDialog: Boolean) {
storyViewerPlaybackStore.update { it.copy(isDisplayingDeleteDialog = isDisplayingDeleteDialog) }
}
fun onDeleteDismissed() {
dismissSubject.onNext(StoryViewerDialog.Type.DELETE)
fun setIsDisplayingViewsAndRepliesDialog(isDisplayingViewsAndRepliesDialog: Boolean) {
storyViewerPlaybackStore.update { it.copy(isDisplayingViewsAndRepliesDialog = isDisplayingViewsAndRepliesDialog) }
}
fun onDismissContextMenu() {
dismissSubject.onNext(StoryViewerDialog.Type.CONTEXT_MENU)
fun setIsDisplayingDirectReplyDialog(isDisplayingDirectReplyDialog: Boolean) {
storyViewerPlaybackStore.update { it.copy(isDisplayingDirectReplyDialog = isDisplayingDirectReplyDialog) }
}
fun onViewsAndRepliesSheetDismissed() {
dismissSubject.onNext(StoryViewerDialog.Type.VIEWS_AND_REPLIES)
fun setIsDisplayingCaptionOverlay(isDisplayingCaptionOverlay: Boolean) {
storyViewerPlaybackStore.update { it.copy(isDisplayingCaptionOverlay = isDisplayingCaptionOverlay) }
}
fun onDirectReplyDismissed() {
dismissSubject.onNext(StoryViewerDialog.Type.DIRECT_REPLY)
fun setIsUserTouching(isUserTouching: Boolean) {
storyViewerPlaybackStore.update { it.copy(isUserTouching = isUserTouching) }
}
fun setAreSegmentsInitialized(areSegmentsInitialized: Boolean) {
storyViewerPlaybackStore.update { it.copy(areSegmentsInitialized = areSegmentsInitialized) }
}
private fun resolveSwipeToReplyState(state: StoryViewerPageState, index: Int = state.selectedPostIndex): StoryViewerPageState.ReplyState {

Wyświetl plik

@ -0,0 +1,22 @@
package org.thoughtcrime.securesms.stories.viewer.page
data class StoryViewerPlaybackState(
val areSegmentsInitialized: Boolean = false,
val isUserTouching: Boolean = false,
val isDisplayingForwardDialog: Boolean = false,
val isDisplayingDeleteDialog: Boolean = false,
val isDisplayingContextMenu: Boolean = false,
val isDisplayingViewsAndRepliesDialog: Boolean = false,
val isDisplayingDirectReplyDialog: Boolean = false,
val isDisplayingCaptionOverlay: Boolean = false
) {
val isPaused: Boolean = !areSegmentsInitialized ||
isUserTouching ||
isDisplayingCaptionOverlay ||
isDisplayingForwardDialog ||
isDisplayingDeleteDialog ||
isDisplayingContextMenu ||
isDisplayingViewsAndRepliesDialog ||
isDisplayingDirectReplyDialog ||
isDisplayingCaptionOverlay
}

Wyświetl plik

@ -122,7 +122,7 @@ class StoryDirectReplyDialogFragment :
override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
storyViewerPageViewModel.onDirectReplyDismissed()
storyViewerPageViewModel.setIsDisplayingDirectReplyDialog(false)
}
companion object {

Wyświetl plik

@ -66,7 +66,7 @@ class StoryGroupReplyBottomSheetDialogFragment : FixedRoundedCornerBottomSheetDi
override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
storyViewerPageViewModel.onViewsAndRepliesSheetDismissed()
storyViewerPageViewModel.setIsDisplayingViewsAndRepliesDialog(false)
}
override fun onStartDirectReply(recipientId: RecipientId) {

Wyświetl plik

@ -102,7 +102,7 @@ class StoryViewsAndRepliesDialogFragment : FixedRoundedCornerBottomSheetDialogFr
override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
storyViewerPageViewModel.onViewsAndRepliesSheetDismissed()
storyViewerPageViewModel.setIsDisplayingViewsAndRepliesDialog(false)
}
override fun onStartDirectReply(recipientId: RecipientId) {

Wyświetl plik

@ -40,7 +40,7 @@ class StoryViewsBottomSheetDialogFragment : FixedRoundedCornerBottomSheetDialogF
override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
storyViewerPageViewModel.onViewsAndRepliesSheetDismissed()
storyViewerPageViewModel.setIsDisplayingViewsAndRepliesDialog(false)
}
companion object {