|
@@ -0,0 +1,212 @@
|
|
|
+/*
|
|
|
+ * Nextcloud Android client application
|
|
|
+ *
|
|
|
+ * @author Álvaro Brey
|
|
|
+ * Copyright (C) 2022 Álvaro Brey
|
|
|
+ * Copyright (C) 2022 Nextcloud GmbH
|
|
|
+ *
|
|
|
+ * This program is free software; you can redistribute it and/or
|
|
|
+ * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
|
|
|
+ * License as published by the Free Software Foundation; either
|
|
|
+ * version 3 of the License, or any later version.
|
|
|
+ *
|
|
|
+ * This program 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 AFFERO GENERAL PUBLIC LICENSE for more details.
|
|
|
+ *
|
|
|
+ * You should have received a copy of the GNU Affero General Public
|
|
|
+ * License along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
+ *
|
|
|
+ */
|
|
|
+
|
|
|
+package com.owncloud.android.ui.preview
|
|
|
+
|
|
|
+import android.app.Activity
|
|
|
+import android.app.Dialog
|
|
|
+import android.os.Build
|
|
|
+import android.view.View
|
|
|
+import android.view.ViewGroup
|
|
|
+import android.view.Window
|
|
|
+import androidx.core.view.WindowCompat
|
|
|
+import androidx.core.view.WindowInsetsCompat
|
|
|
+import androidx.core.view.WindowInsetsControllerCompat
|
|
|
+import com.google.android.exoplayer2.ExoPlayer
|
|
|
+import com.google.android.exoplayer2.Player
|
|
|
+import com.google.android.exoplayer2.ui.StyledPlayerView
|
|
|
+import com.nextcloud.client.media.ExoplayerListener
|
|
|
+import com.nextcloud.client.media.NextcloudExoPlayer
|
|
|
+import com.nextcloud.common.NextcloudClient
|
|
|
+import com.owncloud.android.R
|
|
|
+import com.owncloud.android.databinding.DialogPreviewVideoBinding
|
|
|
+import com.owncloud.android.lib.common.utils.Log_OC
|
|
|
+
|
|
|
+/**
|
|
|
+ * Transfers a previously playing video to a fullscreen dialog, and handles the switch back to the previous player
|
|
|
+ * when closed
|
|
|
+ *
|
|
|
+ * @param activity the Activity hosting the original non-fullscreen player
|
|
|
+ * @param sourceExoPlayer the ExoPlayer playing the video
|
|
|
+ * @param sourceView the original non-fullscreen surface that [sourceExoPlayer] is linked to
|
|
|
+ */
|
|
|
+class PreviewVideoFullscreenDialog(
|
|
|
+ private val activity: Activity,
|
|
|
+ nextcloudClient: NextcloudClient,
|
|
|
+ private val sourceExoPlayer: ExoPlayer,
|
|
|
+ private val sourceView: StyledPlayerView
|
|
|
+) : Dialog(sourceView.context, android.R.style.Theme_Black_NoTitleBar_Fullscreen) {
|
|
|
+
|
|
|
+ private val binding: DialogPreviewVideoBinding = DialogPreviewVideoBinding.inflate(layoutInflater)
|
|
|
+ private var playingStateListener: Player.Listener? = null
|
|
|
+
|
|
|
+ /**
|
|
|
+ * exoPlayer instance used for this view, either the original one or a new one in specific cases.
|
|
|
+ * @see getShouldUseRotatedVideoWorkaround
|
|
|
+ */
|
|
|
+ private val mExoPlayer: ExoPlayer
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Videos with rotation metadata present a bug in sdk < 30 where they are rotated incorrectly and stretched when
|
|
|
+ * the video is resumed on a new surface. To work around this, in those circumstances we'll create a new ExoPlayer
|
|
|
+ * instance, which is slower but should avoid the bug.
|
|
|
+ */
|
|
|
+ private val shouldUseRotatedVideoWorkaround
|
|
|
+ get() = Build.VERSION.SDK_INT < Build.VERSION_CODES.R && isRotatedVideo()
|
|
|
+
|
|
|
+ init {
|
|
|
+ addContentView(
|
|
|
+ binding.root,
|
|
|
+ ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
|
|
|
+ )
|
|
|
+ mExoPlayer = getExoPlayer(nextcloudClient)
|
|
|
+ if (shouldUseRotatedVideoWorkaround) {
|
|
|
+ sourceExoPlayer.currentMediaItem?.let { mExoPlayer.setMediaItem(it, sourceExoPlayer.currentPosition) }
|
|
|
+ binding.videoPlayer.player = mExoPlayer
|
|
|
+ mExoPlayer.prepare()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun isRotatedVideo(): Boolean {
|
|
|
+ val videoFormat = sourceExoPlayer.videoFormat
|
|
|
+ return videoFormat != null && videoFormat.rotationDegrees != 0
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun getExoPlayer(nextcloudClient: NextcloudClient): ExoPlayer {
|
|
|
+ return if (shouldUseRotatedVideoWorkaround) {
|
|
|
+ Log_OC.d(TAG, "Using new ExoPlayer instance to deal with rotated video")
|
|
|
+ NextcloudExoPlayer
|
|
|
+ .createNextcloudExoplayer(sourceView.context, nextcloudClient)
|
|
|
+ .apply {
|
|
|
+ addListener(ExoplayerListener(sourceView.context, binding.videoPlayer, this))
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ sourceExoPlayer
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ override fun show() {
|
|
|
+ val isPlaying = sourceExoPlayer.isPlaying
|
|
|
+ if (isPlaying) {
|
|
|
+ sourceExoPlayer.pause()
|
|
|
+ }
|
|
|
+ enableImmersiveMode()
|
|
|
+ setOnShowListener {
|
|
|
+ switchTargetViewFromSource()
|
|
|
+ setListeners()
|
|
|
+ if (isPlaying) {
|
|
|
+ mExoPlayer.play()
|
|
|
+ }
|
|
|
+ binding.videoPlayer.showController()
|
|
|
+ }
|
|
|
+ super.show()
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun switchTargetViewFromSource() {
|
|
|
+ if (shouldUseRotatedVideoWorkaround) {
|
|
|
+ mExoPlayer.seekTo(sourceExoPlayer.currentPosition)
|
|
|
+ } else {
|
|
|
+ StyledPlayerView.switchTargetView(sourceExoPlayer, sourceView, binding.videoPlayer)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun setListeners() {
|
|
|
+ binding.root.findViewById<View>(R.id.exo_exit_fs).setOnClickListener { onBackPressed() }
|
|
|
+ val pauseButton: View = binding.root.findViewById(R.id.exo_pause)
|
|
|
+ pauseButton.setOnClickListener { sourceExoPlayer.pause() }
|
|
|
+ val playButton: View = binding.root.findViewById(R.id.exo_play)
|
|
|
+ playButton.setOnClickListener { sourceExoPlayer.play() }
|
|
|
+
|
|
|
+ val playListener = object : Player.Listener {
|
|
|
+ override fun onIsPlayingChanged(isPlaying: Boolean) {
|
|
|
+ super.onIsPlayingChanged(isPlaying)
|
|
|
+ if (isPlaying) {
|
|
|
+ playButton.visibility = View.GONE
|
|
|
+ pauseButton.visibility = View.VISIBLE
|
|
|
+ } else {
|
|
|
+ playButton.visibility = View.VISIBLE
|
|
|
+ pauseButton.visibility = View.GONE
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ mExoPlayer.addListener(playListener)
|
|
|
+ playingStateListener = playListener
|
|
|
+ }
|
|
|
+
|
|
|
+ override fun onBackPressed() {
|
|
|
+ val isPlaying = mExoPlayer.isPlaying
|
|
|
+ if (isPlaying) {
|
|
|
+ mExoPlayer.pause()
|
|
|
+ }
|
|
|
+ disableImmersiveMode()
|
|
|
+ setOnDismissListener {
|
|
|
+ playingStateListener?.let {
|
|
|
+ mExoPlayer.removeListener(it)
|
|
|
+ }
|
|
|
+ switchTargetViewToSource()
|
|
|
+ if (isPlaying) {
|
|
|
+ sourceExoPlayer.play()
|
|
|
+ }
|
|
|
+ sourceView.showController()
|
|
|
+ }
|
|
|
+ dismiss()
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun switchTargetViewToSource() {
|
|
|
+ if (shouldUseRotatedVideoWorkaround) {
|
|
|
+ sourceExoPlayer.seekTo(mExoPlayer.currentPosition)
|
|
|
+ } else {
|
|
|
+ StyledPlayerView.switchTargetView(sourceExoPlayer, binding.videoPlayer, sourceView)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun enableImmersiveMode() {
|
|
|
+ // for immersive mode to work properly, need to disable statusbar on activity window, but nav bar in dialog
|
|
|
+ // otherwise dialog navbar is not hidden, or statusbar padding is the wrong color
|
|
|
+ activity.window?.let {
|
|
|
+ hideInset(it, WindowInsetsCompat.Type.statusBars())
|
|
|
+ }
|
|
|
+ window?.let {
|
|
|
+ hideInset(it, WindowInsetsCompat.Type.systemBars())
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun hideInset(window: Window, type: Int) {
|
|
|
+ val windowInsetsController =
|
|
|
+ WindowCompat.getInsetsController(window, window.decorView)
|
|
|
+ windowInsetsController.systemBarsBehavior =
|
|
|
+ WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
|
|
+ windowInsetsController.hide(type)
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun disableImmersiveMode() {
|
|
|
+ activity.window?.let {
|
|
|
+ val windowInsetsController =
|
|
|
+ WindowCompat.getInsetsController(it, it.decorView)
|
|
|
+ windowInsetsController.show(WindowInsetsCompat.Type.systemBars())
|
|
|
+ } ?: return
|
|
|
+ }
|
|
|
+
|
|
|
+ companion object {
|
|
|
+ private val TAG = PreviewVideoFullscreenDialog::class.simpleName
|
|
|
+ }
|
|
|
+}
|