2018-10-15 19:56:11 +02:00
|
|
|
/* Copyright 2017 Andrew Dawson
|
|
|
|
*
|
|
|
|
* This file is a part of Tusky.
|
|
|
|
*
|
|
|
|
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
|
|
|
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
|
|
|
* License, or (at your option) any later version.
|
|
|
|
*
|
|
|
|
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
|
|
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
|
|
|
* Public License for more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
|
|
|
* see <http://www.gnu.org/licenses>. */
|
|
|
|
|
|
|
|
package com.keylesspalace.tusky.fragment
|
|
|
|
|
|
|
|
import android.animation.Animator
|
|
|
|
import android.animation.AnimatorListenerAdapter
|
|
|
|
import android.annotation.SuppressLint
|
2022-12-05 14:33:38 +01:00
|
|
|
import android.content.Context
|
2023-08-10 19:31:55 +02:00
|
|
|
import android.graphics.drawable.Drawable
|
|
|
|
import android.os.Build
|
2018-10-15 19:56:11 +02:00
|
|
|
import android.os.Bundle
|
|
|
|
import android.os.Handler
|
|
|
|
import android.os.Looper
|
2021-09-17 21:55:54 +02:00
|
|
|
import android.text.method.ScrollingMovementMethod
|
2022-12-05 14:33:38 +01:00
|
|
|
import android.view.GestureDetector
|
2023-08-10 19:31:55 +02:00
|
|
|
import android.view.Gravity
|
2018-10-15 19:56:11 +02:00
|
|
|
import android.view.LayoutInflater
|
2022-12-05 14:33:38 +01:00
|
|
|
import android.view.MotionEvent
|
2018-10-15 19:56:11 +02:00
|
|
|
import android.view.View
|
|
|
|
import android.view.ViewGroup
|
2023-08-10 19:31:55 +02:00
|
|
|
import android.widget.FrameLayout
|
|
|
|
import android.widget.LinearLayout
|
|
|
|
import androidx.annotation.OptIn
|
2022-12-05 14:33:38 +01:00
|
|
|
import androidx.core.view.GestureDetectorCompat
|
2023-08-10 19:31:55 +02:00
|
|
|
import androidx.media3.common.MediaItem
|
|
|
|
import androidx.media3.common.PlaybackException
|
|
|
|
import androidx.media3.common.Player
|
|
|
|
import androidx.media3.common.util.UnstableApi
|
|
|
|
import androidx.media3.datasource.DefaultDataSource
|
|
|
|
import androidx.media3.datasource.okhttp.OkHttpDataSource
|
|
|
|
import androidx.media3.exoplayer.ExoPlayer
|
|
|
|
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
|
|
|
|
import androidx.media3.exoplayer.util.EventLogger
|
|
|
|
import androidx.media3.ui.AspectRatioFrameLayout
|
|
|
|
import com.bumptech.glide.Glide
|
|
|
|
import com.bumptech.glide.request.target.CustomTarget
|
|
|
|
import com.bumptech.glide.request.transition.Transition
|
|
|
|
import com.google.android.material.snackbar.Snackbar
|
|
|
|
import com.keylesspalace.tusky.BuildConfig
|
|
|
|
import com.keylesspalace.tusky.R
|
2018-10-15 19:56:11 +02:00
|
|
|
import com.keylesspalace.tusky.ViewMediaActivity
|
2021-03-13 21:27:20 +01:00
|
|
|
import com.keylesspalace.tusky.databinding.FragmentViewVideoBinding
|
2023-08-10 19:31:55 +02:00
|
|
|
import com.keylesspalace.tusky.di.Injectable
|
2018-10-15 19:56:11 +02:00
|
|
|
import com.keylesspalace.tusky.entity.Attachment
|
|
|
|
import com.keylesspalace.tusky.util.hide
|
2023-08-10 19:31:55 +02:00
|
|
|
import com.keylesspalace.tusky.util.viewBinding
|
2018-11-01 14:52:22 +01:00
|
|
|
import com.keylesspalace.tusky.util.visible
|
2023-08-10 19:31:55 +02:00
|
|
|
import javax.inject.Inject
|
2022-12-05 14:33:38 +01:00
|
|
|
import kotlin.math.abs
|
2024-01-04 17:00:55 +01:00
|
|
|
import okhttp3.OkHttpClient
|
2018-10-15 19:56:11 +02:00
|
|
|
|
2023-08-10 19:31:55 +02:00
|
|
|
@UnstableApi
|
|
|
|
class ViewVideoFragment : ViewMediaFragment(), Injectable {
|
2022-12-05 14:33:38 +01:00
|
|
|
interface VideoActionsListener {
|
|
|
|
fun onDismiss()
|
|
|
|
}
|
2021-03-13 21:27:20 +01:00
|
|
|
|
2023-08-10 19:31:55 +02:00
|
|
|
@Inject
|
|
|
|
lateinit var okHttpClient: OkHttpClient
|
|
|
|
|
|
|
|
private val binding by viewBinding(FragmentViewVideoBinding::bind)
|
2021-03-13 21:27:20 +01:00
|
|
|
|
2022-12-05 14:33:38 +01:00
|
|
|
private lateinit var videoActionsListener: VideoActionsListener
|
2018-10-15 19:56:11 +02:00
|
|
|
private lateinit var toolbar: View
|
|
|
|
private val handler = Handler(Looper.getMainLooper())
|
|
|
|
private val hideToolbar = Runnable {
|
|
|
|
// Hoist toolbar hiding to activity so it can track state across different fragments
|
|
|
|
// This is explicitly stored as runnable so that we pass it to the handler later for cancellation
|
|
|
|
mediaActivity.onPhotoTap()
|
|
|
|
}
|
|
|
|
private lateinit var mediaActivity: ViewMediaActivity
|
2023-08-10 19:31:55 +02:00
|
|
|
private lateinit var mediaPlayerListener: Player.Listener
|
2020-01-16 19:01:02 +01:00
|
|
|
private var isAudio = false
|
2018-10-15 19:56:11 +02:00
|
|
|
|
2023-08-10 19:31:55 +02:00
|
|
|
private lateinit var mediaAttachment: Attachment
|
|
|
|
|
|
|
|
private var player: ExoPlayer? = null
|
|
|
|
|
|
|
|
/** The saved seek position, if the fragment is being resumed */
|
|
|
|
private var savedSeekPosition: Long = 0
|
|
|
|
|
|
|
|
private lateinit var mediaSourceFactory: DefaultMediaSourceFactory
|
2023-02-20 19:58:37 +01:00
|
|
|
|
Regularize show/hide logic for video player scrub/play controls (fixes #4073) (#4117)
When viewing a video in Tusky, there is a top toolbar where the
description is shown and the bottom toolbar where play, forward,
backward, and scrub controls are found. In both Tusky 23 and the new
media3 video player code, the logic for showing these toolbars is
*unrelated*; Tusky catches tap events and shows and hides the
description, and the Android media library separately catches tap events
and shows and hides the bottom toolbar. Meanwhile, Tusky and the Android
media library each separately manage a set of logic for auto-hiding
their respective toolbars after a certain number of seconds has passed.
This all results in several problems:
- The top and bottom toolbars can desync, so that one is visible and the
other is not, and tapping to show/hide after this will only swap which
one is visible. This happens *every* time you switch to another
application then back to Tusky while the video player is up.
- You can also desync the top and bottom toolbars in this way by simply
tapping very rapidly.
- The autohide logic was difficult for us to control or customize,
because it was partially hidden inside the Android libraries (relevant
because under media3, the autohide delay increased from 3 to something
like 5 or 6 seconds).
In this patch, I disabled all auto- and tap-based show/hide logic in
media3 and set the Tusky-side show/hide to directly control the media3
toolbar. I then audited the code with printfs until I understood the
state machine of show/hide, and removed anything irrational (some code
was either unreachable, or redundant; either these lines were broken in
the media3 transition, or they never worked).¹
While doing this, I made two policy changes:
- As discussed on Matrix, the autohide delay is now 4 seconds. (In
discussions with users on Mastodon, some complained the previous 3
seconds was too short; but in my opinion and [I think?] charlag's, the
new 5 seconds is too long).
- In the pre-existing code, if the user has hidden the controls, and
they switch to another app and back, the controls display for 4 seconds
then re-hide themselves, just like if the video had been presented for
the first time. I think this is good and kept it— *however* I made a
decision if the user intentionally taps to display the controls, *then*
switches to another app and back, the controls should *not* auto-hide,
because the user most recently requested those controls be shown.
Tests I performed on the final PR (successfully):
- Start video. Expect: toolbar+description hides after 4 seconds.
- Start video. Pause. Resume. Expect: t+d hides after 4 seconds.
- Start video. Wait 4 seconds until t+d hide. Switch to other app.
Switch back. Expect: t+d reappears, then hides after 4 seconds.
- Start video. Wait 4 seconds until t+d hide. Tap to show t+d. Switch to
other app. Switch back. Expect: t+d appear, do NOT autohide.
- Start video. Before 4 seconds up, switch to other app. Switch back.
Expect: t+d reappears, then hides after 4 seconds.
- Start video. Pause. Resume. Before 4 seconds up, switch to other app.
Switch back. Expect: t+d reappears, then hides after 4 seconds.
- Start video. Wait 4 seconds until t+d hide. Tap rapidly over and over
for many seconds. Expect: Nothing weird
- Start *audio*. Expect: At no point does controller autohide, not even
if I switch to another app and back, but I can hide it by manually
tapping
These tests were performed on Android 13. There is an entirely separate
`Build.VERSION.SDK_INT <= 23` path I did not test, but Android Studio
says this is dead code (I think it thinks our minimum SDK is higher than
that?)
---
<small>¹ Incidentally, the underlying cause of #4073 (the show/resume
part of it anyway) turned out to be that the STATE_READY event was being
received not just on video load but also a second time on app resume,
causing certain parts of the initialization code to run a second time
although the fragment had already been fully initialized.</small>
2023-11-23 08:32:01 +01:00
|
|
|
/** Have we received at least one "READY" event? */
|
|
|
|
private var haveStarted = false
|
|
|
|
|
|
|
|
/** Is there a pending autohide? (We can't rely on Android's tracking because that clears on suspend.) */
|
|
|
|
private var pendingHideToolbar = false
|
|
|
|
|
|
|
|
/** Prevent the next play start from queueing a toolbar hide. */
|
|
|
|
private var suppressNextHideToolbar = false
|
|
|
|
|
2022-12-05 14:33:38 +01:00
|
|
|
override fun onAttach(context: Context) {
|
|
|
|
super.onAttach(context)
|
|
|
|
|
2023-08-10 19:31:55 +02:00
|
|
|
mediaSourceFactory = DefaultMediaSourceFactory(context)
|
|
|
|
.setDataSourceFactory(DefaultDataSource.Factory(context, OkHttpDataSource.Factory(okHttpClient)))
|
2018-10-15 19:56:11 +02:00
|
|
|
|
2023-08-10 19:31:55 +02:00
|
|
|
videoActionsListener = context as VideoActionsListener
|
2022-12-30 11:22:39 +01:00
|
|
|
}
|
|
|
|
|
2023-08-10 19:31:55 +02:00
|
|
|
@SuppressLint("PrivateResource", "MissingInflatedId")
|
2024-01-04 17:00:55 +01:00
|
|
|
override fun onCreateView(
|
|
|
|
inflater: LayoutInflater,
|
|
|
|
container: ViewGroup?,
|
|
|
|
savedInstanceState: Bundle?
|
|
|
|
): View {
|
2023-08-10 19:31:55 +02:00
|
|
|
mediaActivity = activity as ViewMediaActivity
|
|
|
|
toolbar = mediaActivity.toolbar
|
|
|
|
val rootView = inflater.inflate(R.layout.fragment_view_video, container, false)
|
2022-12-30 11:22:39 +01:00
|
|
|
|
2023-08-10 19:31:55 +02:00
|
|
|
// Move the controls to the bottom of the screen, with enough bottom margin to clear the seekbar
|
2024-01-04 17:00:55 +01:00
|
|
|
val controls = rootView.findViewById<LinearLayout>(
|
|
|
|
androidx.media3.ui.R.id.exo_center_controls
|
|
|
|
)
|
2023-08-10 19:31:55 +02:00
|
|
|
val layoutParams = controls.layoutParams as FrameLayout.LayoutParams
|
|
|
|
layoutParams.gravity = Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM
|
|
|
|
layoutParams.bottomMargin = rootView.context.resources.getDimension(androidx.media3.ui.R.dimen.exo_styled_bottom_bar_height)
|
|
|
|
.toInt()
|
|
|
|
controls.layoutParams = layoutParams
|
|
|
|
|
|
|
|
return rootView
|
2018-10-15 19:56:11 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@SuppressLint("ClickableViewAccessibility")
|
2023-08-10 19:31:55 +02:00
|
|
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
|
|
super.onViewCreated(view, savedInstanceState)
|
|
|
|
val attachment = arguments?.getParcelable<Attachment>(ARG_ATTACHMENT)
|
|
|
|
?: throw IllegalArgumentException("attachment has to be set")
|
2019-10-01 18:24:09 +02:00
|
|
|
|
2023-08-10 19:31:55 +02:00
|
|
|
val url = attachment.url
|
|
|
|
isAudio = attachment.type == Attachment.Type.AUDIO
|
2023-01-27 21:52:29 +01:00
|
|
|
|
2023-08-10 19:31:55 +02:00
|
|
|
/**
|
|
|
|
* Handle single taps, flings, and dragging
|
|
|
|
*/
|
|
|
|
val touchListener = object : View.OnTouchListener {
|
|
|
|
var lastY = 0f
|
|
|
|
|
|
|
|
/** The view that contains the playing content */
|
|
|
|
// binding.videoView is fullscreen, and includes the controls, so don't use that
|
|
|
|
// when scaling in response to the user dragging on the screen
|
2024-01-04 17:00:55 +01:00
|
|
|
val contentFrame = binding.videoView.findViewById<AspectRatioFrameLayout>(
|
|
|
|
androidx.media3.ui.R.id.exo_content_frame
|
|
|
|
)
|
2023-08-10 19:31:55 +02:00
|
|
|
|
|
|
|
/** Handle taps and flings */
|
|
|
|
val simpleGestureDetector = GestureDetectorCompat(
|
|
|
|
requireContext(),
|
|
|
|
object : GestureDetector.SimpleOnGestureListener() {
|
|
|
|
override fun onDown(e: MotionEvent) = true
|
|
|
|
|
|
|
|
/** A single tap should show/hide the media description */
|
|
|
|
override fun onSingleTapUp(e: MotionEvent): Boolean {
|
|
|
|
mediaActivity.onPhotoTap()
|
Regularize show/hide logic for video player scrub/play controls (fixes #4073) (#4117)
When viewing a video in Tusky, there is a top toolbar where the
description is shown and the bottom toolbar where play, forward,
backward, and scrub controls are found. In both Tusky 23 and the new
media3 video player code, the logic for showing these toolbars is
*unrelated*; Tusky catches tap events and shows and hides the
description, and the Android media library separately catches tap events
and shows and hides the bottom toolbar. Meanwhile, Tusky and the Android
media library each separately manage a set of logic for auto-hiding
their respective toolbars after a certain number of seconds has passed.
This all results in several problems:
- The top and bottom toolbars can desync, so that one is visible and the
other is not, and tapping to show/hide after this will only swap which
one is visible. This happens *every* time you switch to another
application then back to Tusky while the video player is up.
- You can also desync the top and bottom toolbars in this way by simply
tapping very rapidly.
- The autohide logic was difficult for us to control or customize,
because it was partially hidden inside the Android libraries (relevant
because under media3, the autohide delay increased from 3 to something
like 5 or 6 seconds).
In this patch, I disabled all auto- and tap-based show/hide logic in
media3 and set the Tusky-side show/hide to directly control the media3
toolbar. I then audited the code with printfs until I understood the
state machine of show/hide, and removed anything irrational (some code
was either unreachable, or redundant; either these lines were broken in
the media3 transition, or they never worked).¹
While doing this, I made two policy changes:
- As discussed on Matrix, the autohide delay is now 4 seconds. (In
discussions with users on Mastodon, some complained the previous 3
seconds was too short; but in my opinion and [I think?] charlag's, the
new 5 seconds is too long).
- In the pre-existing code, if the user has hidden the controls, and
they switch to another app and back, the controls display for 4 seconds
then re-hide themselves, just like if the video had been presented for
the first time. I think this is good and kept it— *however* I made a
decision if the user intentionally taps to display the controls, *then*
switches to another app and back, the controls should *not* auto-hide,
because the user most recently requested those controls be shown.
Tests I performed on the final PR (successfully):
- Start video. Expect: toolbar+description hides after 4 seconds.
- Start video. Pause. Resume. Expect: t+d hides after 4 seconds.
- Start video. Wait 4 seconds until t+d hide. Switch to other app.
Switch back. Expect: t+d reappears, then hides after 4 seconds.
- Start video. Wait 4 seconds until t+d hide. Tap to show t+d. Switch to
other app. Switch back. Expect: t+d appear, do NOT autohide.
- Start video. Before 4 seconds up, switch to other app. Switch back.
Expect: t+d reappears, then hides after 4 seconds.
- Start video. Pause. Resume. Before 4 seconds up, switch to other app.
Switch back. Expect: t+d reappears, then hides after 4 seconds.
- Start video. Wait 4 seconds until t+d hide. Tap rapidly over and over
for many seconds. Expect: Nothing weird
- Start *audio*. Expect: At no point does controller autohide, not even
if I switch to another app and back, but I can hide it by manually
tapping
These tests were performed on Android 13. There is an entirely separate
`Build.VERSION.SDK_INT <= 23` path I did not test, but Android Studio
says this is dead code (I think it thinks our minimum SDK is higher than
that?)
---
<small>¹ Incidentally, the underlying cause of #4073 (the show/resume
part of it anyway) turned out to be that the STATE_READY event was being
received not just on video load but also a second time on app resume,
causing certain parts of the initialization code to run a second time
although the fragment had already been fully initialized.</small>
2023-11-23 08:32:01 +01:00
|
|
|
return true // Do not pass gestures through to media3
|
2023-08-10 19:31:55 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/** A fling up/down should dismiss the fragment */
|
|
|
|
override fun onFling(
|
|
|
|
e1: MotionEvent,
|
|
|
|
e2: MotionEvent,
|
|
|
|
velocityX: Float,
|
|
|
|
velocityY: Float
|
|
|
|
): Boolean {
|
|
|
|
if (abs(velocityY) > abs(velocityX)) {
|
|
|
|
videoActionsListener.onDismiss()
|
|
|
|
return true
|
|
|
|
}
|
Regularize show/hide logic for video player scrub/play controls (fixes #4073) (#4117)
When viewing a video in Tusky, there is a top toolbar where the
description is shown and the bottom toolbar where play, forward,
backward, and scrub controls are found. In both Tusky 23 and the new
media3 video player code, the logic for showing these toolbars is
*unrelated*; Tusky catches tap events and shows and hides the
description, and the Android media library separately catches tap events
and shows and hides the bottom toolbar. Meanwhile, Tusky and the Android
media library each separately manage a set of logic for auto-hiding
their respective toolbars after a certain number of seconds has passed.
This all results in several problems:
- The top and bottom toolbars can desync, so that one is visible and the
other is not, and tapping to show/hide after this will only swap which
one is visible. This happens *every* time you switch to another
application then back to Tusky while the video player is up.
- You can also desync the top and bottom toolbars in this way by simply
tapping very rapidly.
- The autohide logic was difficult for us to control or customize,
because it was partially hidden inside the Android libraries (relevant
because under media3, the autohide delay increased from 3 to something
like 5 or 6 seconds).
In this patch, I disabled all auto- and tap-based show/hide logic in
media3 and set the Tusky-side show/hide to directly control the media3
toolbar. I then audited the code with printfs until I understood the
state machine of show/hide, and removed anything irrational (some code
was either unreachable, or redundant; either these lines were broken in
the media3 transition, or they never worked).¹
While doing this, I made two policy changes:
- As discussed on Matrix, the autohide delay is now 4 seconds. (In
discussions with users on Mastodon, some complained the previous 3
seconds was too short; but in my opinion and [I think?] charlag's, the
new 5 seconds is too long).
- In the pre-existing code, if the user has hidden the controls, and
they switch to another app and back, the controls display for 4 seconds
then re-hide themselves, just like if the video had been presented for
the first time. I think this is good and kept it— *however* I made a
decision if the user intentionally taps to display the controls, *then*
switches to another app and back, the controls should *not* auto-hide,
because the user most recently requested those controls be shown.
Tests I performed on the final PR (successfully):
- Start video. Expect: toolbar+description hides after 4 seconds.
- Start video. Pause. Resume. Expect: t+d hides after 4 seconds.
- Start video. Wait 4 seconds until t+d hide. Switch to other app.
Switch back. Expect: t+d reappears, then hides after 4 seconds.
- Start video. Wait 4 seconds until t+d hide. Tap to show t+d. Switch to
other app. Switch back. Expect: t+d appear, do NOT autohide.
- Start video. Before 4 seconds up, switch to other app. Switch back.
Expect: t+d reappears, then hides after 4 seconds.
- Start video. Pause. Resume. Before 4 seconds up, switch to other app.
Switch back. Expect: t+d reappears, then hides after 4 seconds.
- Start video. Wait 4 seconds until t+d hide. Tap rapidly over and over
for many seconds. Expect: Nothing weird
- Start *audio*. Expect: At no point does controller autohide, not even
if I switch to another app and back, but I can hide it by manually
tapping
These tests were performed on Android 13. There is an entirely separate
`Build.VERSION.SDK_INT <= 23` path I did not test, but Android Studio
says this is dead code (I think it thinks our minimum SDK is higher than
that?)
---
<small>¹ Incidentally, the underlying cause of #4073 (the show/resume
part of it anyway) turned out to be that the STATE_READY event was being
received not just on video load but also a second time on app resume,
causing certain parts of the initialization code to run a second time
although the fragment had already been fully initialized.</small>
2023-11-23 08:32:01 +01:00
|
|
|
return true // Do not pass gestures through to media3
|
2023-08-10 19:31:55 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
)
|
2020-01-16 19:01:02 +01:00
|
|
|
|
2023-08-10 19:31:55 +02:00
|
|
|
@SuppressLint("ClickableViewAccessibility")
|
|
|
|
override fun onTouch(v: View?, event: MotionEvent): Boolean {
|
|
|
|
// Track movement, and scale / translate the video display accordingly
|
|
|
|
if (event.action == MotionEvent.ACTION_DOWN) {
|
|
|
|
lastY = event.rawY
|
|
|
|
} else if (event.pointerCount == 1 && event.action == MotionEvent.ACTION_MOVE) {
|
|
|
|
val diff = event.rawY - lastY
|
|
|
|
if (contentFrame.translationY != 0f || abs(diff) > 40) {
|
|
|
|
contentFrame.translationY += diff
|
|
|
|
val scale = (-abs(contentFrame.translationY) / 720 + 1).coerceAtLeast(0.5f)
|
|
|
|
contentFrame.scaleY = scale
|
|
|
|
contentFrame.scaleX = scale
|
|
|
|
lastY = event.rawY
|
|
|
|
}
|
|
|
|
} else if (event.action == MotionEvent.ACTION_UP || event.action == MotionEvent.ACTION_CANCEL) {
|
|
|
|
if (abs(contentFrame.translationY) > 180) {
|
|
|
|
videoActionsListener.onDismiss()
|
|
|
|
} else {
|
|
|
|
contentFrame.animate().translationY(0f).scaleX(1f).scaleY(1f).start()
|
2020-01-16 19:01:02 +01:00
|
|
|
}
|
|
|
|
}
|
2023-08-10 19:31:55 +02:00
|
|
|
|
|
|
|
simpleGestureDetector.onTouchEvent(event)
|
|
|
|
|
Regularize show/hide logic for video player scrub/play controls (fixes #4073) (#4117)
When viewing a video in Tusky, there is a top toolbar where the
description is shown and the bottom toolbar where play, forward,
backward, and scrub controls are found. In both Tusky 23 and the new
media3 video player code, the logic for showing these toolbars is
*unrelated*; Tusky catches tap events and shows and hides the
description, and the Android media library separately catches tap events
and shows and hides the bottom toolbar. Meanwhile, Tusky and the Android
media library each separately manage a set of logic for auto-hiding
their respective toolbars after a certain number of seconds has passed.
This all results in several problems:
- The top and bottom toolbars can desync, so that one is visible and the
other is not, and tapping to show/hide after this will only swap which
one is visible. This happens *every* time you switch to another
application then back to Tusky while the video player is up.
- You can also desync the top and bottom toolbars in this way by simply
tapping very rapidly.
- The autohide logic was difficult for us to control or customize,
because it was partially hidden inside the Android libraries (relevant
because under media3, the autohide delay increased from 3 to something
like 5 or 6 seconds).
In this patch, I disabled all auto- and tap-based show/hide logic in
media3 and set the Tusky-side show/hide to directly control the media3
toolbar. I then audited the code with printfs until I understood the
state machine of show/hide, and removed anything irrational (some code
was either unreachable, or redundant; either these lines were broken in
the media3 transition, or they never worked).¹
While doing this, I made two policy changes:
- As discussed on Matrix, the autohide delay is now 4 seconds. (In
discussions with users on Mastodon, some complained the previous 3
seconds was too short; but in my opinion and [I think?] charlag's, the
new 5 seconds is too long).
- In the pre-existing code, if the user has hidden the controls, and
they switch to another app and back, the controls display for 4 seconds
then re-hide themselves, just like if the video had been presented for
the first time. I think this is good and kept it— *however* I made a
decision if the user intentionally taps to display the controls, *then*
switches to another app and back, the controls should *not* auto-hide,
because the user most recently requested those controls be shown.
Tests I performed on the final PR (successfully):
- Start video. Expect: toolbar+description hides after 4 seconds.
- Start video. Pause. Resume. Expect: t+d hides after 4 seconds.
- Start video. Wait 4 seconds until t+d hide. Switch to other app.
Switch back. Expect: t+d reappears, then hides after 4 seconds.
- Start video. Wait 4 seconds until t+d hide. Tap to show t+d. Switch to
other app. Switch back. Expect: t+d appear, do NOT autohide.
- Start video. Before 4 seconds up, switch to other app. Switch back.
Expect: t+d reappears, then hides after 4 seconds.
- Start video. Pause. Resume. Before 4 seconds up, switch to other app.
Switch back. Expect: t+d reappears, then hides after 4 seconds.
- Start video. Wait 4 seconds until t+d hide. Tap rapidly over and over
for many seconds. Expect: Nothing weird
- Start *audio*. Expect: At no point does controller autohide, not even
if I switch to another app and back, but I can hide it by manually
tapping
These tests were performed on Android 13. There is an entirely separate
`Build.VERSION.SDK_INT <= 23` path I did not test, but Android Studio
says this is dead code (I think it thinks our minimum SDK is higher than
that?)
---
<small>¹ Incidentally, the underlying cause of #4073 (the show/resume
part of it anyway) turned out to be that the STATE_READY event was being
received not just on video load but also a second time on app resume,
causing certain parts of the initialization code to run a second time
although the fragment had already been fully initialized.</small>
2023-11-23 08:32:01 +01:00
|
|
|
// Do not pass gestures through to media3
|
|
|
|
// We have to do this because otherwise taps to hide will be double-handled and media3 will re-show itself
|
|
|
|
// media3 has a property to disable "hide on tap" but "show on tap" is unconditional
|
|
|
|
return true
|
2020-01-16 19:01:02 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-10 19:31:55 +02:00
|
|
|
mediaPlayerListener = object : Player.Listener {
|
|
|
|
@SuppressLint("ClickableViewAccessibility", "SyntheticAccessor")
|
|
|
|
@OptIn(UnstableApi::class)
|
|
|
|
override fun onPlaybackStateChanged(playbackState: Int) {
|
|
|
|
when (playbackState) {
|
|
|
|
Player.STATE_READY -> {
|
Regularize show/hide logic for video player scrub/play controls (fixes #4073) (#4117)
When viewing a video in Tusky, there is a top toolbar where the
description is shown and the bottom toolbar where play, forward,
backward, and scrub controls are found. In both Tusky 23 and the new
media3 video player code, the logic for showing these toolbars is
*unrelated*; Tusky catches tap events and shows and hides the
description, and the Android media library separately catches tap events
and shows and hides the bottom toolbar. Meanwhile, Tusky and the Android
media library each separately manage a set of logic for auto-hiding
their respective toolbars after a certain number of seconds has passed.
This all results in several problems:
- The top and bottom toolbars can desync, so that one is visible and the
other is not, and tapping to show/hide after this will only swap which
one is visible. This happens *every* time you switch to another
application then back to Tusky while the video player is up.
- You can also desync the top and bottom toolbars in this way by simply
tapping very rapidly.
- The autohide logic was difficult for us to control or customize,
because it was partially hidden inside the Android libraries (relevant
because under media3, the autohide delay increased from 3 to something
like 5 or 6 seconds).
In this patch, I disabled all auto- and tap-based show/hide logic in
media3 and set the Tusky-side show/hide to directly control the media3
toolbar. I then audited the code with printfs until I understood the
state machine of show/hide, and removed anything irrational (some code
was either unreachable, or redundant; either these lines were broken in
the media3 transition, or they never worked).¹
While doing this, I made two policy changes:
- As discussed on Matrix, the autohide delay is now 4 seconds. (In
discussions with users on Mastodon, some complained the previous 3
seconds was too short; but in my opinion and [I think?] charlag's, the
new 5 seconds is too long).
- In the pre-existing code, if the user has hidden the controls, and
they switch to another app and back, the controls display for 4 seconds
then re-hide themselves, just like if the video had been presented for
the first time. I think this is good and kept it— *however* I made a
decision if the user intentionally taps to display the controls, *then*
switches to another app and back, the controls should *not* auto-hide,
because the user most recently requested those controls be shown.
Tests I performed on the final PR (successfully):
- Start video. Expect: toolbar+description hides after 4 seconds.
- Start video. Pause. Resume. Expect: t+d hides after 4 seconds.
- Start video. Wait 4 seconds until t+d hide. Switch to other app.
Switch back. Expect: t+d reappears, then hides after 4 seconds.
- Start video. Wait 4 seconds until t+d hide. Tap to show t+d. Switch to
other app. Switch back. Expect: t+d appear, do NOT autohide.
- Start video. Before 4 seconds up, switch to other app. Switch back.
Expect: t+d reappears, then hides after 4 seconds.
- Start video. Pause. Resume. Before 4 seconds up, switch to other app.
Switch back. Expect: t+d reappears, then hides after 4 seconds.
- Start video. Wait 4 seconds until t+d hide. Tap rapidly over and over
for many seconds. Expect: Nothing weird
- Start *audio*. Expect: At no point does controller autohide, not even
if I switch to another app and back, but I can hide it by manually
tapping
These tests were performed on Android 13. There is an entirely separate
`Build.VERSION.SDK_INT <= 23` path I did not test, but Android Studio
says this is dead code (I think it thinks our minimum SDK is higher than
that?)
---
<small>¹ Incidentally, the underlying cause of #4073 (the show/resume
part of it anyway) turned out to be that the STATE_READY event was being
received not just on video load but also a second time on app resume,
causing certain parts of the initialization code to run a second time
although the fragment had already been fully initialized.</small>
2023-11-23 08:32:01 +01:00
|
|
|
if (!haveStarted) {
|
|
|
|
// Wait until the media is loaded before accepting taps as we don't want toolbar to
|
|
|
|
// be hidden until then.
|
|
|
|
binding.videoView.setOnTouchListener(touchListener)
|
|
|
|
|
|
|
|
binding.progressBar.hide()
|
|
|
|
binding.videoView.useController = true
|
|
|
|
binding.videoView.showController()
|
|
|
|
haveStarted = true
|
|
|
|
} else {
|
|
|
|
// This isn't a real "done loading"; this is a resume event after backgrounding.
|
|
|
|
if (mediaActivity.isToolbarVisible) {
|
|
|
|
// Before suspend, the toolbar/description were visible, so description is visible already.
|
|
|
|
// But media3 will have automatically hidden the video controls on suspend, so we need to match the description state.
|
|
|
|
binding.videoView.showController()
|
|
|
|
if (!pendingHideToolbar) {
|
|
|
|
suppressNextHideToolbar = true // The user most recently asked us to show the toolbar, so don't hide it when play starts.
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
mediaActivity.onPhotoTap()
|
|
|
|
}
|
|
|
|
}
|
2023-08-10 19:31:55 +02:00
|
|
|
}
|
|
|
|
else -> { /* do nothing */ }
|
2020-01-16 19:01:02 +01:00
|
|
|
}
|
|
|
|
}
|
2023-02-23 19:41:16 +01:00
|
|
|
|
2023-08-10 19:31:55 +02:00
|
|
|
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
|
|
|
if (isAudio) return
|
|
|
|
if (isPlaying) {
|
Regularize show/hide logic for video player scrub/play controls (fixes #4073) (#4117)
When viewing a video in Tusky, there is a top toolbar where the
description is shown and the bottom toolbar where play, forward,
backward, and scrub controls are found. In both Tusky 23 and the new
media3 video player code, the logic for showing these toolbars is
*unrelated*; Tusky catches tap events and shows and hides the
description, and the Android media library separately catches tap events
and shows and hides the bottom toolbar. Meanwhile, Tusky and the Android
media library each separately manage a set of logic for auto-hiding
their respective toolbars after a certain number of seconds has passed.
This all results in several problems:
- The top and bottom toolbars can desync, so that one is visible and the
other is not, and tapping to show/hide after this will only swap which
one is visible. This happens *every* time you switch to another
application then back to Tusky while the video player is up.
- You can also desync the top and bottom toolbars in this way by simply
tapping very rapidly.
- The autohide logic was difficult for us to control or customize,
because it was partially hidden inside the Android libraries (relevant
because under media3, the autohide delay increased from 3 to something
like 5 or 6 seconds).
In this patch, I disabled all auto- and tap-based show/hide logic in
media3 and set the Tusky-side show/hide to directly control the media3
toolbar. I then audited the code with printfs until I understood the
state machine of show/hide, and removed anything irrational (some code
was either unreachable, or redundant; either these lines were broken in
the media3 transition, or they never worked).¹
While doing this, I made two policy changes:
- As discussed on Matrix, the autohide delay is now 4 seconds. (In
discussions with users on Mastodon, some complained the previous 3
seconds was too short; but in my opinion and [I think?] charlag's, the
new 5 seconds is too long).
- In the pre-existing code, if the user has hidden the controls, and
they switch to another app and back, the controls display for 4 seconds
then re-hide themselves, just like if the video had been presented for
the first time. I think this is good and kept it— *however* I made a
decision if the user intentionally taps to display the controls, *then*
switches to another app and back, the controls should *not* auto-hide,
because the user most recently requested those controls be shown.
Tests I performed on the final PR (successfully):
- Start video. Expect: toolbar+description hides after 4 seconds.
- Start video. Pause. Resume. Expect: t+d hides after 4 seconds.
- Start video. Wait 4 seconds until t+d hide. Switch to other app.
Switch back. Expect: t+d reappears, then hides after 4 seconds.
- Start video. Wait 4 seconds until t+d hide. Tap to show t+d. Switch to
other app. Switch back. Expect: t+d appear, do NOT autohide.
- Start video. Before 4 seconds up, switch to other app. Switch back.
Expect: t+d reappears, then hides after 4 seconds.
- Start video. Pause. Resume. Before 4 seconds up, switch to other app.
Switch back. Expect: t+d reappears, then hides after 4 seconds.
- Start video. Wait 4 seconds until t+d hide. Tap rapidly over and over
for many seconds. Expect: Nothing weird
- Start *audio*. Expect: At no point does controller autohide, not even
if I switch to another app and back, but I can hide it by manually
tapping
These tests were performed on Android 13. There is an entirely separate
`Build.VERSION.SDK_INT <= 23` path I did not test, but Android Studio
says this is dead code (I think it thinks our minimum SDK is higher than
that?)
---
<small>¹ Incidentally, the underlying cause of #4073 (the show/resume
part of it anyway) turned out to be that the STATE_READY event was being
received not just on video load but also a second time on app resume,
causing certain parts of the initialization code to run a second time
although the fragment had already been fully initialized.</small>
2023-11-23 08:32:01 +01:00
|
|
|
if (suppressNextHideToolbar) {
|
|
|
|
suppressNextHideToolbar = false
|
|
|
|
} else {
|
|
|
|
hideToolbarAfterDelay(TOOLBAR_HIDE_DELAY_MS)
|
|
|
|
}
|
2023-08-10 19:31:55 +02:00
|
|
|
} else {
|
2023-02-23 19:41:16 +01:00
|
|
|
handler.removeCallbacks(hideToolbar)
|
|
|
|
}
|
|
|
|
}
|
2019-10-01 18:24:09 +02:00
|
|
|
|
2023-08-10 19:31:55 +02:00
|
|
|
@SuppressLint("SyntheticAccessor")
|
|
|
|
override fun onPlayerError(error: PlaybackException) {
|
|
|
|
binding.progressBar.hide()
|
|
|
|
val message = getString(
|
|
|
|
R.string.error_media_playback,
|
|
|
|
error.cause?.message ?: error.message
|
|
|
|
)
|
|
|
|
Snackbar.make(binding.root, message, Snackbar.LENGTH_INDEFINITE)
|
|
|
|
.setTextMaxLines(10)
|
|
|
|
.setAction(R.string.action_retry) { player?.prepare() }
|
|
|
|
.show()
|
2020-01-16 19:01:02 +01:00
|
|
|
}
|
2023-08-10 19:31:55 +02:00
|
|
|
}
|
2020-01-16 19:01:02 +01:00
|
|
|
|
2023-08-10 19:31:55 +02:00
|
|
|
savedSeekPosition = savedInstanceState?.getLong(SEEK_POSITION) ?: 0
|
|
|
|
|
|
|
|
mediaAttachment = attachment
|
|
|
|
|
|
|
|
finalizeViewSetup(url, attachment.previewUrl, attachment.description)
|
|
|
|
}
|
2023-02-23 19:41:16 +01:00
|
|
|
|
2023-08-10 19:31:55 +02:00
|
|
|
override fun onStart() {
|
|
|
|
super.onStart()
|
|
|
|
if (Build.VERSION.SDK_INT > 23) {
|
|
|
|
initializePlayer()
|
|
|
|
binding.videoView.onResume()
|
2018-10-15 19:56:11 +02:00
|
|
|
}
|
2023-08-10 19:31:55 +02:00
|
|
|
}
|
2018-10-15 19:56:11 +02:00
|
|
|
|
2023-08-10 19:31:55 +02:00
|
|
|
override fun onResume() {
|
|
|
|
super.onResume()
|
|
|
|
|
|
|
|
if (Build.VERSION.SDK_INT <= 23 || player == null) {
|
|
|
|
initializePlayer()
|
Regularize show/hide logic for video player scrub/play controls (fixes #4073) (#4117)
When viewing a video in Tusky, there is a top toolbar where the
description is shown and the bottom toolbar where play, forward,
backward, and scrub controls are found. In both Tusky 23 and the new
media3 video player code, the logic for showing these toolbars is
*unrelated*; Tusky catches tap events and shows and hides the
description, and the Android media library separately catches tap events
and shows and hides the bottom toolbar. Meanwhile, Tusky and the Android
media library each separately manage a set of logic for auto-hiding
their respective toolbars after a certain number of seconds has passed.
This all results in several problems:
- The top and bottom toolbars can desync, so that one is visible and the
other is not, and tapping to show/hide after this will only swap which
one is visible. This happens *every* time you switch to another
application then back to Tusky while the video player is up.
- You can also desync the top and bottom toolbars in this way by simply
tapping very rapidly.
- The autohide logic was difficult for us to control or customize,
because it was partially hidden inside the Android libraries (relevant
because under media3, the autohide delay increased from 3 to something
like 5 or 6 seconds).
In this patch, I disabled all auto- and tap-based show/hide logic in
media3 and set the Tusky-side show/hide to directly control the media3
toolbar. I then audited the code with printfs until I understood the
state machine of show/hide, and removed anything irrational (some code
was either unreachable, or redundant; either these lines were broken in
the media3 transition, or they never worked).¹
While doing this, I made two policy changes:
- As discussed on Matrix, the autohide delay is now 4 seconds. (In
discussions with users on Mastodon, some complained the previous 3
seconds was too short; but in my opinion and [I think?] charlag's, the
new 5 seconds is too long).
- In the pre-existing code, if the user has hidden the controls, and
they switch to another app and back, the controls display for 4 seconds
then re-hide themselves, just like if the video had been presented for
the first time. I think this is good and kept it— *however* I made a
decision if the user intentionally taps to display the controls, *then*
switches to another app and back, the controls should *not* auto-hide,
because the user most recently requested those controls be shown.
Tests I performed on the final PR (successfully):
- Start video. Expect: toolbar+description hides after 4 seconds.
- Start video. Pause. Resume. Expect: t+d hides after 4 seconds.
- Start video. Wait 4 seconds until t+d hide. Switch to other app.
Switch back. Expect: t+d reappears, then hides after 4 seconds.
- Start video. Wait 4 seconds until t+d hide. Tap to show t+d. Switch to
other app. Switch back. Expect: t+d appear, do NOT autohide.
- Start video. Before 4 seconds up, switch to other app. Switch back.
Expect: t+d reappears, then hides after 4 seconds.
- Start video. Pause. Resume. Before 4 seconds up, switch to other app.
Switch back. Expect: t+d reappears, then hides after 4 seconds.
- Start video. Wait 4 seconds until t+d hide. Tap rapidly over and over
for many seconds. Expect: Nothing weird
- Start *audio*. Expect: At no point does controller autohide, not even
if I switch to another app and back, but I can hide it by manually
tapping
These tests were performed on Android 13. There is an entirely separate
`Build.VERSION.SDK_INT <= 23` path I did not test, but Android Studio
says this is dead code (I think it thinks our minimum SDK is higher than
that?)
---
<small>¹ Incidentally, the underlying cause of #4073 (the show/resume
part of it anyway) turned out to be that the STATE_READY event was being
received not just on video load but also a second time on app resume,
causing certain parts of the initialization code to run a second time
although the fragment had already been fully initialized.</small>
2023-11-23 08:32:01 +01:00
|
|
|
|
2023-08-10 19:31:55 +02:00
|
|
|
binding.videoView.onResume()
|
2018-10-15 19:56:11 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-10 19:31:55 +02:00
|
|
|
private fun releasePlayer() {
|
|
|
|
player?.let {
|
|
|
|
savedSeekPosition = it.currentPosition
|
|
|
|
it.release()
|
|
|
|
player = null
|
|
|
|
binding.videoView.player = null
|
|
|
|
}
|
2018-10-15 19:56:11 +02:00
|
|
|
}
|
|
|
|
|
2023-08-10 19:31:55 +02:00
|
|
|
override fun onPause() {
|
|
|
|
super.onPause()
|
|
|
|
|
|
|
|
// If <= API 23 then multi-window mode is not available, so this is a good time to
|
|
|
|
// pause everything
|
|
|
|
if (Build.VERSION.SDK_INT <= 23) {
|
|
|
|
binding.videoView.onPause()
|
|
|
|
releasePlayer()
|
|
|
|
handler.removeCallbacks(hideToolbar)
|
|
|
|
}
|
2018-10-15 19:56:11 +02:00
|
|
|
}
|
|
|
|
|
2023-08-10 19:31:55 +02:00
|
|
|
override fun onStop() {
|
|
|
|
super.onStop()
|
2018-10-15 19:56:11 +02:00
|
|
|
|
2023-08-10 19:31:55 +02:00
|
|
|
// If > API 23 then this might be multi-window, and definitely wasn't paused in onPause,
|
|
|
|
// so pause everything now.
|
|
|
|
if (Build.VERSION.SDK_INT > 23) {
|
|
|
|
binding.videoView.onPause()
|
|
|
|
releasePlayer()
|
|
|
|
handler.removeCallbacks(hideToolbar)
|
|
|
|
}
|
|
|
|
}
|
2022-12-05 14:33:38 +01:00
|
|
|
|
2023-08-10 19:31:55 +02:00
|
|
|
override fun onSaveInstanceState(outState: Bundle) {
|
|
|
|
super.onSaveInstanceState(outState)
|
|
|
|
outState.putLong(SEEK_POSITION, savedSeekPosition)
|
|
|
|
}
|
2022-12-05 14:33:38 +01:00
|
|
|
|
2023-08-10 19:31:55 +02:00
|
|
|
private fun initializePlayer() {
|
|
|
|
ExoPlayer.Builder(requireContext())
|
|
|
|
.setMediaSourceFactory(mediaSourceFactory)
|
|
|
|
.build().apply {
|
|
|
|
if (BuildConfig.DEBUG) addAnalyticsListener(EventLogger("$TAG:ExoPlayer"))
|
|
|
|
setMediaItem(MediaItem.fromUri(mediaAttachment.url))
|
|
|
|
addListener(mediaPlayerListener)
|
|
|
|
repeatMode = Player.REPEAT_MODE_ONE
|
|
|
|
playWhenReady = true
|
|
|
|
seekTo(savedSeekPosition)
|
|
|
|
prepare()
|
|
|
|
player = this
|
2022-12-05 14:33:38 +01:00
|
|
|
}
|
2023-08-10 19:31:55 +02:00
|
|
|
|
|
|
|
binding.videoView.player = player
|
|
|
|
|
|
|
|
// Audio-only files might have a preview image. If they do, set it as the artwork
|
|
|
|
if (isAudio) {
|
|
|
|
mediaAttachment.previewUrl?.let { url ->
|
|
|
|
Glide.with(this).load(url).into(object : CustomTarget<Drawable>() {
|
|
|
|
@SuppressLint("SyntheticAccessor")
|
|
|
|
override fun onResourceReady(
|
|
|
|
resource: Drawable,
|
|
|
|
transition: Transition<in Drawable>?
|
|
|
|
) {
|
|
|
|
view ?: return
|
|
|
|
binding.videoView.defaultArtwork = resource
|
|
|
|
}
|
|
|
|
|
|
|
|
@SuppressLint("SyntheticAccessor")
|
|
|
|
override fun onLoadCleared(placeholder: Drawable?) {
|
|
|
|
view ?: return
|
|
|
|
binding.videoView.defaultArtwork = null
|
|
|
|
}
|
|
|
|
})
|
2022-12-05 14:33:38 +01:00
|
|
|
}
|
2023-08-10 19:31:55 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@SuppressLint("ClickableViewAccessibility")
|
|
|
|
override fun setupMediaView(
|
|
|
|
url: String,
|
|
|
|
previewUrl: String?,
|
|
|
|
description: String?,
|
|
|
|
showingDescription: Boolean
|
|
|
|
) {
|
|
|
|
binding.mediaDescription.text = description
|
|
|
|
binding.mediaDescription.visible(showingDescription)
|
|
|
|
binding.mediaDescription.movementMethod = ScrollingMovementMethod()
|
|
|
|
|
|
|
|
// Ensure the description is visible over the video
|
|
|
|
binding.mediaDescription.elevation = binding.videoView.elevation + 1
|
|
|
|
|
|
|
|
binding.videoView.transitionName = url
|
|
|
|
|
|
|
|
binding.videoView.requestFocus()
|
2022-12-05 14:33:38 +01:00
|
|
|
|
2023-08-10 19:31:55 +02:00
|
|
|
if (requireArguments().getBoolean(ARG_START_POSTPONED_TRANSITION)) {
|
|
|
|
mediaActivity.onBringUp()
|
2022-12-05 14:33:38 +01:00
|
|
|
}
|
2023-08-10 19:31:55 +02:00
|
|
|
}
|
2022-12-05 14:33:38 +01:00
|
|
|
|
2023-08-10 19:31:55 +02:00
|
|
|
private fun hideToolbarAfterDelay(delayMilliseconds: Int) {
|
Regularize show/hide logic for video player scrub/play controls (fixes #4073) (#4117)
When viewing a video in Tusky, there is a top toolbar where the
description is shown and the bottom toolbar where play, forward,
backward, and scrub controls are found. In both Tusky 23 and the new
media3 video player code, the logic for showing these toolbars is
*unrelated*; Tusky catches tap events and shows and hides the
description, and the Android media library separately catches tap events
and shows and hides the bottom toolbar. Meanwhile, Tusky and the Android
media library each separately manage a set of logic for auto-hiding
their respective toolbars after a certain number of seconds has passed.
This all results in several problems:
- The top and bottom toolbars can desync, so that one is visible and the
other is not, and tapping to show/hide after this will only swap which
one is visible. This happens *every* time you switch to another
application then back to Tusky while the video player is up.
- You can also desync the top and bottom toolbars in this way by simply
tapping very rapidly.
- The autohide logic was difficult for us to control or customize,
because it was partially hidden inside the Android libraries (relevant
because under media3, the autohide delay increased from 3 to something
like 5 or 6 seconds).
In this patch, I disabled all auto- and tap-based show/hide logic in
media3 and set the Tusky-side show/hide to directly control the media3
toolbar. I then audited the code with printfs until I understood the
state machine of show/hide, and removed anything irrational (some code
was either unreachable, or redundant; either these lines were broken in
the media3 transition, or they never worked).¹
While doing this, I made two policy changes:
- As discussed on Matrix, the autohide delay is now 4 seconds. (In
discussions with users on Mastodon, some complained the previous 3
seconds was too short; but in my opinion and [I think?] charlag's, the
new 5 seconds is too long).
- In the pre-existing code, if the user has hidden the controls, and
they switch to another app and back, the controls display for 4 seconds
then re-hide themselves, just like if the video had been presented for
the first time. I think this is good and kept it— *however* I made a
decision if the user intentionally taps to display the controls, *then*
switches to another app and back, the controls should *not* auto-hide,
because the user most recently requested those controls be shown.
Tests I performed on the final PR (successfully):
- Start video. Expect: toolbar+description hides after 4 seconds.
- Start video. Pause. Resume. Expect: t+d hides after 4 seconds.
- Start video. Wait 4 seconds until t+d hide. Switch to other app.
Switch back. Expect: t+d reappears, then hides after 4 seconds.
- Start video. Wait 4 seconds until t+d hide. Tap to show t+d. Switch to
other app. Switch back. Expect: t+d appear, do NOT autohide.
- Start video. Before 4 seconds up, switch to other app. Switch back.
Expect: t+d reappears, then hides after 4 seconds.
- Start video. Pause. Resume. Before 4 seconds up, switch to other app.
Switch back. Expect: t+d reappears, then hides after 4 seconds.
- Start video. Wait 4 seconds until t+d hide. Tap rapidly over and over
for many seconds. Expect: Nothing weird
- Start *audio*. Expect: At no point does controller autohide, not even
if I switch to another app and back, but I can hide it by manually
tapping
These tests were performed on Android 13. There is an entirely separate
`Build.VERSION.SDK_INT <= 23` path I did not test, but Android Studio
says this is dead code (I think it thinks our minimum SDK is higher than
that?)
---
<small>¹ Incidentally, the underlying cause of #4073 (the show/resume
part of it anyway) turned out to be that the STATE_READY event was being
received not just on video load but also a second time on app resume,
causing certain parts of the initialization code to run a second time
although the fragment had already been fully initialized.</small>
2023-11-23 08:32:01 +01:00
|
|
|
pendingHideToolbar = true
|
2023-08-10 19:31:55 +02:00
|
|
|
handler.postDelayed(hideToolbar, delayMilliseconds.toLong())
|
2018-10-15 19:56:11 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
override fun onToolbarVisibilityChange(visible: Boolean) {
|
2023-08-10 19:31:55 +02:00
|
|
|
if (!userVisibleHint) {
|
2018-10-15 19:56:11 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
isDescriptionVisible = showingDescription && visible
|
|
|
|
val alpha = if (isDescriptionVisible) 1.0f else 0.0f
|
2020-01-16 19:01:02 +01:00
|
|
|
if (isDescriptionVisible) {
|
|
|
|
// If to be visible, need to make visible immediately and animate alpha
|
2021-03-13 21:27:20 +01:00
|
|
|
binding.mediaDescription.alpha = 0.0f
|
|
|
|
binding.mediaDescription.visible(isDescriptionVisible)
|
2020-01-16 19:01:02 +01:00
|
|
|
}
|
|
|
|
|
2021-03-13 21:27:20 +01:00
|
|
|
binding.mediaDescription.animate().alpha(alpha)
|
2021-06-28 21:13:24 +02:00
|
|
|
.setListener(object : AnimatorListenerAdapter() {
|
2023-08-10 19:31:55 +02:00
|
|
|
@SuppressLint("SyntheticAccessor")
|
2021-06-28 21:13:24 +02:00
|
|
|
override fun onAnimationEnd(animation: Animator) {
|
2023-08-10 19:31:55 +02:00
|
|
|
view ?: return
|
|
|
|
binding.mediaDescription.visible(isDescriptionVisible)
|
2021-06-28 21:13:24 +02:00
|
|
|
animation.removeListener(this)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.start()
|
2018-10-15 19:56:11 +02:00
|
|
|
|
Regularize show/hide logic for video player scrub/play controls (fixes #4073) (#4117)
When viewing a video in Tusky, there is a top toolbar where the
description is shown and the bottom toolbar where play, forward,
backward, and scrub controls are found. In both Tusky 23 and the new
media3 video player code, the logic for showing these toolbars is
*unrelated*; Tusky catches tap events and shows and hides the
description, and the Android media library separately catches tap events
and shows and hides the bottom toolbar. Meanwhile, Tusky and the Android
media library each separately manage a set of logic for auto-hiding
their respective toolbars after a certain number of seconds has passed.
This all results in several problems:
- The top and bottom toolbars can desync, so that one is visible and the
other is not, and tapping to show/hide after this will only swap which
one is visible. This happens *every* time you switch to another
application then back to Tusky while the video player is up.
- You can also desync the top and bottom toolbars in this way by simply
tapping very rapidly.
- The autohide logic was difficult for us to control or customize,
because it was partially hidden inside the Android libraries (relevant
because under media3, the autohide delay increased from 3 to something
like 5 or 6 seconds).
In this patch, I disabled all auto- and tap-based show/hide logic in
media3 and set the Tusky-side show/hide to directly control the media3
toolbar. I then audited the code with printfs until I understood the
state machine of show/hide, and removed anything irrational (some code
was either unreachable, or redundant; either these lines were broken in
the media3 transition, or they never worked).¹
While doing this, I made two policy changes:
- As discussed on Matrix, the autohide delay is now 4 seconds. (In
discussions with users on Mastodon, some complained the previous 3
seconds was too short; but in my opinion and [I think?] charlag's, the
new 5 seconds is too long).
- In the pre-existing code, if the user has hidden the controls, and
they switch to another app and back, the controls display for 4 seconds
then re-hide themselves, just like if the video had been presented for
the first time. I think this is good and kept it— *however* I made a
decision if the user intentionally taps to display the controls, *then*
switches to another app and back, the controls should *not* auto-hide,
because the user most recently requested those controls be shown.
Tests I performed on the final PR (successfully):
- Start video. Expect: toolbar+description hides after 4 seconds.
- Start video. Pause. Resume. Expect: t+d hides after 4 seconds.
- Start video. Wait 4 seconds until t+d hide. Switch to other app.
Switch back. Expect: t+d reappears, then hides after 4 seconds.
- Start video. Wait 4 seconds until t+d hide. Tap to show t+d. Switch to
other app. Switch back. Expect: t+d appear, do NOT autohide.
- Start video. Before 4 seconds up, switch to other app. Switch back.
Expect: t+d reappears, then hides after 4 seconds.
- Start video. Pause. Resume. Before 4 seconds up, switch to other app.
Switch back. Expect: t+d reappears, then hides after 4 seconds.
- Start video. Wait 4 seconds until t+d hide. Tap rapidly over and over
for many seconds. Expect: Nothing weird
- Start *audio*. Expect: At no point does controller autohide, not even
if I switch to another app and back, but I can hide it by manually
tapping
These tests were performed on Android 13. There is an entirely separate
`Build.VERSION.SDK_INT <= 23` path I did not test, but Android Studio
says this is dead code (I think it thinks our minimum SDK is higher than
that?)
---
<small>¹ Incidentally, the underlying cause of #4073 (the show/resume
part of it anyway) turned out to be that the STATE_READY event was being
received not just on video load but also a second time on app resume,
causing certain parts of the initialization code to run a second time
although the fragment had already been fully initialized.</small>
2023-11-23 08:32:01 +01:00
|
|
|
// media3 controls bar
|
|
|
|
if (visible) {
|
|
|
|
binding.videoView.showController()
|
2018-10-15 19:56:11 +02:00
|
|
|
} else {
|
Regularize show/hide logic for video player scrub/play controls (fixes #4073) (#4117)
When viewing a video in Tusky, there is a top toolbar where the
description is shown and the bottom toolbar where play, forward,
backward, and scrub controls are found. In both Tusky 23 and the new
media3 video player code, the logic for showing these toolbars is
*unrelated*; Tusky catches tap events and shows and hides the
description, and the Android media library separately catches tap events
and shows and hides the bottom toolbar. Meanwhile, Tusky and the Android
media library each separately manage a set of logic for auto-hiding
their respective toolbars after a certain number of seconds has passed.
This all results in several problems:
- The top and bottom toolbars can desync, so that one is visible and the
other is not, and tapping to show/hide after this will only swap which
one is visible. This happens *every* time you switch to another
application then back to Tusky while the video player is up.
- You can also desync the top and bottom toolbars in this way by simply
tapping very rapidly.
- The autohide logic was difficult for us to control or customize,
because it was partially hidden inside the Android libraries (relevant
because under media3, the autohide delay increased from 3 to something
like 5 or 6 seconds).
In this patch, I disabled all auto- and tap-based show/hide logic in
media3 and set the Tusky-side show/hide to directly control the media3
toolbar. I then audited the code with printfs until I understood the
state machine of show/hide, and removed anything irrational (some code
was either unreachable, or redundant; either these lines were broken in
the media3 transition, or they never worked).¹
While doing this, I made two policy changes:
- As discussed on Matrix, the autohide delay is now 4 seconds. (In
discussions with users on Mastodon, some complained the previous 3
seconds was too short; but in my opinion and [I think?] charlag's, the
new 5 seconds is too long).
- In the pre-existing code, if the user has hidden the controls, and
they switch to another app and back, the controls display for 4 seconds
then re-hide themselves, just like if the video had been presented for
the first time. I think this is good and kept it— *however* I made a
decision if the user intentionally taps to display the controls, *then*
switches to another app and back, the controls should *not* auto-hide,
because the user most recently requested those controls be shown.
Tests I performed on the final PR (successfully):
- Start video. Expect: toolbar+description hides after 4 seconds.
- Start video. Pause. Resume. Expect: t+d hides after 4 seconds.
- Start video. Wait 4 seconds until t+d hide. Switch to other app.
Switch back. Expect: t+d reappears, then hides after 4 seconds.
- Start video. Wait 4 seconds until t+d hide. Tap to show t+d. Switch to
other app. Switch back. Expect: t+d appear, do NOT autohide.
- Start video. Before 4 seconds up, switch to other app. Switch back.
Expect: t+d reappears, then hides after 4 seconds.
- Start video. Pause. Resume. Before 4 seconds up, switch to other app.
Switch back. Expect: t+d reappears, then hides after 4 seconds.
- Start video. Wait 4 seconds until t+d hide. Tap rapidly over and over
for many seconds. Expect: Nothing weird
- Start *audio*. Expect: At no point does controller autohide, not even
if I switch to another app and back, but I can hide it by manually
tapping
These tests were performed on Android 13. There is an entirely separate
`Build.VERSION.SDK_INT <= 23` path I did not test, but Android Studio
says this is dead code (I think it thinks our minimum SDK is higher than
that?)
---
<small>¹ Incidentally, the underlying cause of #4073 (the show/resume
part of it anyway) turned out to be that the STATE_READY event was being
received not just on video load but also a second time on app resume,
causing certain parts of the initialization code to run a second time
although the fragment had already been fully initialized.</small>
2023-11-23 08:32:01 +01:00
|
|
|
binding.videoView.hideController()
|
2018-10-15 19:56:11 +02:00
|
|
|
}
|
Regularize show/hide logic for video player scrub/play controls (fixes #4073) (#4117)
When viewing a video in Tusky, there is a top toolbar where the
description is shown and the bottom toolbar where play, forward,
backward, and scrub controls are found. In both Tusky 23 and the new
media3 video player code, the logic for showing these toolbars is
*unrelated*; Tusky catches tap events and shows and hides the
description, and the Android media library separately catches tap events
and shows and hides the bottom toolbar. Meanwhile, Tusky and the Android
media library each separately manage a set of logic for auto-hiding
their respective toolbars after a certain number of seconds has passed.
This all results in several problems:
- The top and bottom toolbars can desync, so that one is visible and the
other is not, and tapping to show/hide after this will only swap which
one is visible. This happens *every* time you switch to another
application then back to Tusky while the video player is up.
- You can also desync the top and bottom toolbars in this way by simply
tapping very rapidly.
- The autohide logic was difficult for us to control or customize,
because it was partially hidden inside the Android libraries (relevant
because under media3, the autohide delay increased from 3 to something
like 5 or 6 seconds).
In this patch, I disabled all auto- and tap-based show/hide logic in
media3 and set the Tusky-side show/hide to directly control the media3
toolbar. I then audited the code with printfs until I understood the
state machine of show/hide, and removed anything irrational (some code
was either unreachable, or redundant; either these lines were broken in
the media3 transition, or they never worked).¹
While doing this, I made two policy changes:
- As discussed on Matrix, the autohide delay is now 4 seconds. (In
discussions with users on Mastodon, some complained the previous 3
seconds was too short; but in my opinion and [I think?] charlag's, the
new 5 seconds is too long).
- In the pre-existing code, if the user has hidden the controls, and
they switch to another app and back, the controls display for 4 seconds
then re-hide themselves, just like if the video had been presented for
the first time. I think this is good and kept it— *however* I made a
decision if the user intentionally taps to display the controls, *then*
switches to another app and back, the controls should *not* auto-hide,
because the user most recently requested those controls be shown.
Tests I performed on the final PR (successfully):
- Start video. Expect: toolbar+description hides after 4 seconds.
- Start video. Pause. Resume. Expect: t+d hides after 4 seconds.
- Start video. Wait 4 seconds until t+d hide. Switch to other app.
Switch back. Expect: t+d reappears, then hides after 4 seconds.
- Start video. Wait 4 seconds until t+d hide. Tap to show t+d. Switch to
other app. Switch back. Expect: t+d appear, do NOT autohide.
- Start video. Before 4 seconds up, switch to other app. Switch back.
Expect: t+d reappears, then hides after 4 seconds.
- Start video. Pause. Resume. Before 4 seconds up, switch to other app.
Switch back. Expect: t+d reappears, then hides after 4 seconds.
- Start video. Wait 4 seconds until t+d hide. Tap rapidly over and over
for many seconds. Expect: Nothing weird
- Start *audio*. Expect: At no point does controller autohide, not even
if I switch to another app and back, but I can hide it by manually
tapping
These tests were performed on Android 13. There is an entirely separate
`Build.VERSION.SDK_INT <= 23` path I did not test, but Android Studio
says this is dead code (I think it thinks our minimum SDK is higher than
that?)
---
<small>¹ Incidentally, the underlying cause of #4073 (the show/resume
part of it anyway) turned out to be that the STATE_READY event was being
received not just on video load but also a second time on app resume,
causing certain parts of the initialization code to run a second time
although the fragment had already been fully initialized.</small>
2023-11-23 08:32:01 +01:00
|
|
|
|
|
|
|
// Either the user just requested toolbar display, or we just hid it.
|
|
|
|
// Either way, any pending hides are no longer appropriate.
|
|
|
|
pendingHideToolbar = false
|
|
|
|
handler.removeCallbacks(hideToolbar)
|
2018-10-15 19:56:11 +02:00
|
|
|
}
|
2019-08-04 20:22:57 +02:00
|
|
|
|
2023-08-10 19:31:55 +02:00
|
|
|
override fun onTransitionEnd() { }
|
2021-03-13 21:27:20 +01:00
|
|
|
|
2023-08-10 19:31:55 +02:00
|
|
|
companion object {
|
|
|
|
private const val TAG = "ViewVideoFragment"
|
Regularize show/hide logic for video player scrub/play controls (fixes #4073) (#4117)
When viewing a video in Tusky, there is a top toolbar where the
description is shown and the bottom toolbar where play, forward,
backward, and scrub controls are found. In both Tusky 23 and the new
media3 video player code, the logic for showing these toolbars is
*unrelated*; Tusky catches tap events and shows and hides the
description, and the Android media library separately catches tap events
and shows and hides the bottom toolbar. Meanwhile, Tusky and the Android
media library each separately manage a set of logic for auto-hiding
their respective toolbars after a certain number of seconds has passed.
This all results in several problems:
- The top and bottom toolbars can desync, so that one is visible and the
other is not, and tapping to show/hide after this will only swap which
one is visible. This happens *every* time you switch to another
application then back to Tusky while the video player is up.
- You can also desync the top and bottom toolbars in this way by simply
tapping very rapidly.
- The autohide logic was difficult for us to control or customize,
because it was partially hidden inside the Android libraries (relevant
because under media3, the autohide delay increased from 3 to something
like 5 or 6 seconds).
In this patch, I disabled all auto- and tap-based show/hide logic in
media3 and set the Tusky-side show/hide to directly control the media3
toolbar. I then audited the code with printfs until I understood the
state machine of show/hide, and removed anything irrational (some code
was either unreachable, or redundant; either these lines were broken in
the media3 transition, or they never worked).¹
While doing this, I made two policy changes:
- As discussed on Matrix, the autohide delay is now 4 seconds. (In
discussions with users on Mastodon, some complained the previous 3
seconds was too short; but in my opinion and [I think?] charlag's, the
new 5 seconds is too long).
- In the pre-existing code, if the user has hidden the controls, and
they switch to another app and back, the controls display for 4 seconds
then re-hide themselves, just like if the video had been presented for
the first time. I think this is good and kept it— *however* I made a
decision if the user intentionally taps to display the controls, *then*
switches to another app and back, the controls should *not* auto-hide,
because the user most recently requested those controls be shown.
Tests I performed on the final PR (successfully):
- Start video. Expect: toolbar+description hides after 4 seconds.
- Start video. Pause. Resume. Expect: t+d hides after 4 seconds.
- Start video. Wait 4 seconds until t+d hide. Switch to other app.
Switch back. Expect: t+d reappears, then hides after 4 seconds.
- Start video. Wait 4 seconds until t+d hide. Tap to show t+d. Switch to
other app. Switch back. Expect: t+d appear, do NOT autohide.
- Start video. Before 4 seconds up, switch to other app. Switch back.
Expect: t+d reappears, then hides after 4 seconds.
- Start video. Pause. Resume. Before 4 seconds up, switch to other app.
Switch back. Expect: t+d reappears, then hides after 4 seconds.
- Start video. Wait 4 seconds until t+d hide. Tap rapidly over and over
for many seconds. Expect: Nothing weird
- Start *audio*. Expect: At no point does controller autohide, not even
if I switch to another app and back, but I can hide it by manually
tapping
These tests were performed on Android 13. There is an entirely separate
`Build.VERSION.SDK_INT <= 23` path I did not test, but Android Studio
says this is dead code (I think it thinks our minimum SDK is higher than
that?)
---
<small>¹ Incidentally, the underlying cause of #4073 (the show/resume
part of it anyway) turned out to be that the STATE_READY event was being
received not just on video load but also a second time on app resume,
causing certain parts of the initialization code to run a second time
although the fragment had already been fully initialized.</small>
2023-11-23 08:32:01 +01:00
|
|
|
private const val TOOLBAR_HIDE_DELAY_MS = 4_000
|
2023-08-10 19:31:55 +02:00
|
|
|
private const val SEEK_POSITION = "seekPosition"
|
2021-03-13 21:27:20 +01:00
|
|
|
}
|
2018-10-15 19:56:11 +02:00
|
|
|
}
|