kopia lustrzana https://github.com/ryukoposting/Signal-Android
Update playback to match specifications.
rodzic
267efb0763
commit
1bb04035ab
|
@ -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)) {
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Ładowanie…
Reference in New Issue