Update playback to match specifications.

fork-5.53.8
Alex Hart 2022-03-30 16:30:06 -03:00 zatwierdzone przez Cody Henthorne
rodzic 267efb0763
commit 1bb04035ab
10 zmienionych plików z 96 dodań i 50 usunięć

Wyświetl plik

@ -579,7 +579,7 @@ public class MmsDatabase extends MessageDatabase {
@Override
public @NonNull MessageDatabase.Reader getAllStories() {
return new Reader(rawQuery(IS_STORY_CLAUSE, null, true, -1L));
return new Reader(rawQuery(IS_STORY_CLAUSE, null, false, -1L));
}
@Override
@ -660,7 +660,7 @@ public class MmsDatabase extends MessageDatabase {
"FROM " + TABLE_NAME + " JOIN " + ThreadDatabase.TABLE_NAME + " " +
"ON " + TABLE_NAME + "." + THREAD_ID + " = " + ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.ID + " " +
"WHERE " + IS_STORY_CLAUSE + " " +
"ORDER BY " + TABLE_NAME + "." + DATE_SENT + " DESC";
"ORDER BY " + TABLE_NAME + "." + DATE_SENT + " DESC, " + TABLE_NAME + "." + VIEWED_RECEIPT_COUNT + " ASC";
List<RecipientId> recipientIds;
try (Cursor cursor = db.rawQuery(query, null)) {

Wyświetl plik

@ -1,7 +1,6 @@
package org.thoughtcrime.securesms.mediapreview
import android.net.Uri
import io.reactivex.rxjava3.subjects.BehaviorSubject
import org.thoughtcrime.securesms.video.VideoPlayer
/**
@ -10,36 +9,42 @@ import org.thoughtcrime.securesms.video.VideoPlayer
class VideoControlsDelegate {
private val playWhenReady: MutableMap<Uri, Boolean> = mutableMapOf()
private val playerSubject = BehaviorSubject.create<Player>()
private var player: Player? = null
fun getPlayerState(uri: Uri): PlayerState? {
val player = playerSubject.value
val player: Player? = this.player
return if (player?.uri == uri && player.videoPlayer != null) {
PlayerState(uri, player.videoPlayer.playbackPosition, player.videoPlayer.duration)
PlayerState(uri, player.videoPlayer.playbackPosition, player.videoPlayer.duration, player.isGif, player.loopCount)
} else {
null
}
}
fun pause() = playerSubject.value?.videoPlayer?.pause()
fun pause() = player?.videoPlayer?.pause()
fun resume(uri: Uri) {
val player = playerSubject.value
if (player?.uri == uri) {
player.videoPlayer?.play()
player?.videoPlayer?.play()
} else {
playWhenReady[uri] = true
}
playerSubject.value?.videoPlayer?.play()
this.player?.videoPlayer?.play()
}
fun restart() {
playerSubject.value?.videoPlayer?.playbackPosition = 0L
player?.videoPlayer?.playbackPosition = 0L
}
fun attachPlayer(uri: Uri, videoPlayer: VideoPlayer?) {
playerSubject.onNext(Player(uri, videoPlayer))
fun onPlayerPositionDiscontinuity(reason: Int) {
val player = this.player
if (player != null && player.isGif) {
this.player = player.copy(loopCount = if (reason == 0) player.loopCount + 1 else 0)
}
}
fun attachPlayer(uri: Uri, videoPlayer: VideoPlayer?, isGif: Boolean) {
player = Player(uri, videoPlayer, isGif)
if (playWhenReady[uri] == true) {
playWhenReady[uri] = false
@ -48,17 +53,21 @@ class VideoControlsDelegate {
}
fun detachPlayer() {
playerSubject.onNext(Player())
player = Player()
}
private data class Player(
val uri: Uri = Uri.EMPTY,
val videoPlayer: VideoPlayer? = null
val videoPlayer: VideoPlayer? = null,
val isGif: Boolean = false,
val loopCount: Int = 0
)
data class PlayerState(
val mediaUri: Uri,
val position: Long,
val duration: Long
val duration: Long,
val isGif: Boolean,
val loopCount: Int
)
}

Wyświetl plik

@ -48,6 +48,11 @@ public final class VideoMediaPreviewFragment extends MediaPreviewFragment {
videoView.setWindow(requireActivity().getWindow());
videoView.setVideoSource(new VideoSlide(getContext(), uri, size, false), autoPlay);
videoView.setPlayerPositionDiscontinuityCallback((v, r) -> {
if (events.getVideoControlsDelegate() != null) {
events.getVideoControlsDelegate().onPlayerPositionDiscontinuity(r);
}
});
if (isVideoGif) {
videoView.hideControls();
@ -74,7 +79,7 @@ public final class VideoMediaPreviewFragment extends MediaPreviewFragment {
}
if (events.getVideoControlsDelegate() != null) {
events.getVideoControlsDelegate().attachPlayer(getUri(), videoView);
events.getVideoControlsDelegate().attachPlayer(getUri(), videoView, isVideoGif);
}
}

Wyświetl plik

@ -23,6 +23,10 @@ data class StoriesLandingItemData(
-1
} else if (!storyRecipient.isMyStory && other.storyRecipient.isMyStory) {
1
} else if (storyViewState == StoryViewState.UNVIEWED && other.storyViewState != StoryViewState.UNVIEWED) {
-1
} else if (storyViewState != StoryViewState.UNVIEWED && other.storyViewState == StoryViewState.UNVIEWED) {
1
} else {
-dateInMilliseconds.compareTo(other.dateInMilliseconds)
}

Wyświetl plik

@ -72,13 +72,14 @@ class StoriesLandingRepository(context: Context) {
private fun createStoriesLandingItemData(sender: Recipient, messageRecords: List<MessageRecord>): Observable<StoriesLandingItemData> {
val itemDataObservable = Observable.create<StoriesLandingItemData> { emitter ->
fun refresh(sender: Recipient) {
val primaryIndex = messageRecords.indexOfFirst { !it.isOutgoing && it.viewedReceiptCount == 0 }.takeIf { it > -1 } ?: 0
val itemData = StoriesLandingItemData(
storyRecipient = sender,
storyViewState = StoryViewState.NONE,
hasReplies = messageRecords.any { SignalDatabase.mms.getNumberOfStoryReplies(it.id) > 0 },
hasRepliesFromSelf = messageRecords.any { SignalDatabase.mms.hasSelfReplyInStory(it.id) },
isHidden = sender.shouldHideStory(),
primaryStory = ConversationMessage.ConversationMessageFactory.createWithUnresolvedData(context, messageRecords.first()),
primaryStory = ConversationMessage.ConversationMessageFactory.createWithUnresolvedData(context, messageRecords[primaryIndex]),
secondaryStory = if (sender.isMyStory) messageRecords.drop(1).firstOrNull()?.let {
ConversationMessage.ConversationMessageFactory.createWithUnresolvedData(context, it)
} else null

Wyświetl plik

@ -28,7 +28,7 @@ class StoryPost(
override fun isVideo(): Boolean = MediaUtil.isVideo(attachment)
}
class TextContent(uri: Uri, val recordId: Long, hasBody: Boolean) : Content(uri) {
class TextContent(uri: Uri, val recordId: Long, hasBody: Boolean, val length: Int) : Content(uri) {
override val transferState: Int = if (hasBody) AttachmentDatabase.TRANSFER_PROGRESS_DONE else AttachmentDatabase.TRANSFER_PROGRESS_FAILED
override fun isVideo(): Boolean = false

Wyświetl plik

@ -65,6 +65,7 @@ import java.util.Locale
import java.util.concurrent.TimeUnit
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
class StoryViewerPageFragment :
Fragment(R.layout.stories_viewer_fragment_page),
@ -216,7 +217,7 @@ class StoryViewerPageFragment :
return if (attachmentUri != null) {
val playerState = videoControlsDelegate.getPlayerState(attachmentUri)
if (playerState != null) {
playerState.position.toFloat() / playerState.duration
getVideoPlaybackPosition(playerState) / getVideoPlaybackDuration(playerState)
} else {
null
}
@ -266,7 +267,11 @@ class StoryViewerPageFragment :
val durations: Map<Int, Long> = state.posts
.mapIndexed { index, storyPost ->
index to if (storyPost.content.isVideo()) -1L else TimeUnit.SECONDS.toMillis(5)
index to when {
storyPost.content.isVideo() -> -1L
storyPost.content is StoryPost.Content.TextContent -> calculateDurationForText(storyPost.content)
else -> DEFAULT_DURATION
}
}
.toMap()
@ -331,6 +336,28 @@ class StoryViewerPageFragment :
viewModel.setIsDisplayingForwardDialog(false)
}
private fun calculateDurationForText(textContent: StoryPost.Content.TextContent): Long {
val divisionsOf15 = textContent.length / CHARACTERS_PER_SECOND
return TimeUnit.SECONDS.toMillis(divisionsOf15) + MIN_TEXT_STORY_PLAYBACK
}
private fun getVideoPlaybackPosition(playerState: VideoControlsDelegate.PlayerState): Float {
return if (playerState.isGif) {
playerState.position.toFloat() + (playerState.duration * playerState.loopCount)
} else {
playerState.position.toFloat()
}
}
private fun getVideoPlaybackDuration(playerState: VideoControlsDelegate.PlayerState): Long {
return if (playerState.isGif) {
val timeToPlayMinLoops = playerState.duration * MIN_GIF_LOOPS
max(MIN_GIF_PLAYBACK_DURATION, timeToPlayMinLoops)
} else {
min(playerState.duration, MAX_VIDEO_PLAYBACK_DURATION)
}
}
private fun hideChrome() {
animateChrome(0f)
}
@ -678,6 +705,13 @@ class StoryViewerPageFragment :
}
companion object {
private val MAX_VIDEO_PLAYBACK_DURATION: Long = TimeUnit.SECONDS.toMillis(30)
private val MIN_GIF_LOOPS: Long = 3L
private val MIN_GIF_PLAYBACK_DURATION = TimeUnit.SECONDS.toMillis(5)
private val MIN_TEXT_STORY_PLAYBACK = TimeUnit.SECONDS.toMillis(3)
private val CHARACTERS_PER_SECOND = 15L
private val DEFAULT_DURATION = TimeUnit.SECONDS.toMillis(5)
private const val ARG_STORY_RECIPIENT_ID = "arg.story.recipient.id"
private const val ARG_STORY_ID = "arg.story.id"

Wyświetl plik

@ -5,6 +5,7 @@ import android.net.Uri
import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.schedulers.Schedulers
import org.signal.core.util.BreakIteratorCompat
import org.signal.core.util.concurrent.SignalExecutors
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
import org.thoughtcrime.securesms.conversation.ConversationMessage
@ -178,7 +179,8 @@ class StoryViewerPageRepository(context: Context) {
StoryPost.Content.TextContent(
uri = Uri.parse("story_text_post://${record.id}"),
recordId = record.id,
hasBody = canParseToTextStory(record.body)
hasBody = canParseToTextStory(record.body),
length = getTextStoryLength(record.body)
)
} else {
StoryPost.Content.AttachmentContent(
@ -187,6 +189,16 @@ class StoryViewerPageRepository(context: Context) {
}
}
private fun getTextStoryLength(body: String): Int {
return if (canParseToTextStory(body)) {
val breakIteratorCompat = BreakIteratorCompat.getInstance()
breakIteratorCompat.setText(StoryTextPost.parseFrom(Base64.decode(body)).body)
breakIteratorCompat.countBreaks()
} else {
0
}
}
private fun canParseToTextStory(body: String): Boolean {
return if (body.isNotEmpty()) {
try {

Wyświetl plik

@ -49,6 +49,9 @@ class StoryViewerPageViewModel(
val startIndex = if (state.posts.isEmpty() && initialStoryId > 0) {
val initialIndex = posts.indexOfFirst { it.id == initialStoryId }
initialIndex.takeIf { it > -1 } ?: state.selectedPostIndex
} else if (state.posts.isEmpty()) {
val initialIndex = posts.indexOfFirst { !it.conversationMessage.messageRecord.isOutgoing && it.conversationMessage.messageRecord.viewedReceiptCount == 0 }
initialIndex.takeIf { it > -1 } ?: state.selectedPostIndex
} else {
state.selectedPostIndex
}

Wyświetl plik

@ -83,8 +83,8 @@ public class VideoPlayer extends FrameLayout {
this.exoControls = new PlayerControlView(getContext());
this.exoControls.setShowTimeoutMs(-1);
this.exoPlayerListener = new ExoPlayerListener(this, window, playerStateCallback, playerPositionDiscontinuityCallback);
this.playerListener = new Player.Listener() {
this.exoPlayerListener = new ExoPlayerListener();
this.playerListener = new Player.Listener() {
@Override
public void onPlayWhenReadyChanged(boolean playWhenReady, int reason) {
onPlaybackStateChanged(playWhenReady, exoPlayer.getPlaybackState());
@ -244,9 +244,6 @@ public class VideoPlayer extends FrameLayout {
public void setWindow(@Nullable Window window) {
this.window = window;
if (exoPlayerListener != null) {
exoPlayerListener.setWindow(window);
}
}
public void setPlayerStateCallbacks(@Nullable PlayerStateCallback playerStateCallback) {
@ -273,35 +270,16 @@ public class VideoPlayer extends FrameLayout {
}
}
private static class ExoPlayerListener implements Player.Listener {
private final VideoPlayer videoPlayer;
private Window window;
private final PlayerStateCallback playerStateCallback;
private final PlayerPositionDiscontinuityCallback playerPositionDiscontinuityCallback;
ExoPlayerListener(@NonNull VideoPlayer videoPlayer,
@Nullable Window window,
@Nullable PlayerStateCallback playerStateCallback,
@Nullable PlayerPositionDiscontinuityCallback playerPositionDiscontinuityCallback)
{
this.videoPlayer = videoPlayer;
this.window = window;
this.playerStateCallback = playerStateCallback;
this.playerPositionDiscontinuityCallback = playerPositionDiscontinuityCallback;
}
private class ExoPlayerListener implements Player.Listener {
@Override
public void onPlayWhenReadyChanged(boolean playWhenReady, int reason) {
onPlaybackStateChanged(playWhenReady, videoPlayer.exoPlayer.getPlaybackState());
onPlaybackStateChanged(playWhenReady, exoPlayer.getPlaybackState());
}
@Override
public void onPlaybackStateChanged(int playbackState) {
onPlaybackStateChanged(videoPlayer.exoPlayer.getPlayWhenReady(), playbackState);
}
public void setWindow(Window window) {
this.window = window;
onPlaybackStateChanged(exoPlayer.getPlayWhenReady(), playbackState);
}
private void onPlaybackStateChanged(boolean playWhenReady, int playbackState) {
@ -334,7 +312,7 @@ public class VideoPlayer extends FrameLayout {
int reason)
{
if (playerPositionDiscontinuityCallback != null) {
playerPositionDiscontinuityCallback.onPositionDiscontinuity(videoPlayer, reason);
playerPositionDiscontinuityCallback.onPositionDiscontinuity(VideoPlayer.this, reason);
}
}