diff --git a/app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaFragment.java b/app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaFragment.java index 5733950505..271b03325d 100644 --- a/app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaFragment.java @@ -31,6 +31,7 @@ import android.app.Activity; import android.content.Context; import android.content.Intent; import android.content.res.Configuration; +import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.drawable.Drawable; @@ -44,7 +45,9 @@ import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; +import android.view.MotionEvent; import android.view.View; +import android.view.View.OnTouchListener; import android.view.ViewGroup; import com.nextcloud.client.account.User; @@ -82,22 +85,15 @@ import java.util.concurrent.Executors; import javax.inject.Inject; import androidx.annotation.NonNull; -import androidx.annotation.OptIn; import androidx.annotation.StringRes; -import androidx.appcompat.app.ActionBar; -import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.content.res.AppCompatResources; import androidx.core.graphics.drawable.DrawableCompat; -import androidx.core.view.WindowCompat; -import androidx.core.view.WindowInsetsCompat; -import androidx.core.view.WindowInsetsControllerCompat; import androidx.drawerlayout.widget.DrawerLayout; +import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentManager; import androidx.media3.common.MediaItem; -import androidx.media3.common.util.UnstableApi; import androidx.media3.exoplayer.ExoPlayer; -@OptIn(markerClass = UnstableApi.class) /** * This fragment shows a preview of a downloaded media file (audio or video). *

@@ -107,7 +103,7 @@ import androidx.media3.exoplayer.ExoPlayer; * By now, if the {@link OCFile} passed is not downloaded, an {@link IllegalStateException} is generated on * instantiation too. */ -public class PreviewMediaFragment extends FileFragment implements +public class PreviewMediaFragment extends FileFragment implements OnTouchListener, Injectable { private static final String TAG = PreviewMediaFragment.class.getSimpleName(); @@ -144,8 +140,6 @@ public class PreviewMediaFragment extends FileFragment implements private ViewGroup emptyListView; private ExoPlayer exoPlayer; private NextcloudClient nextcloudClient; - private WindowInsetsControllerCompat windowInsetsController; - private ActionBar actionBar; /** * Creates a fragment to preview a file. @@ -260,7 +254,6 @@ public class PreviewMediaFragment extends FileFragment implements if (MimeTypeUtil.isVideo(file)) { binding.exoplayerView.setVisibility(View.VISIBLE); binding.imagePreview.setVisibility(View.GONE); - binding.getRoot().setBackgroundColor(getResources().getColor(R.color.black, null)); } else { binding.exoplayerView.setVisibility(View.GONE); binding.imagePreview.setVisibility(View.VISIBLE); @@ -404,32 +397,9 @@ public class PreviewMediaFragment extends FileFragment implements } } - private void initWindowInsetsController() { - windowInsetsController = WindowCompat.getInsetsController(requireActivity().getWindow(), requireActivity().getWindow().getDecorView()); - windowInsetsController.setSystemBarsBehavior(WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); - } - - private void initActionBar() { - AppCompatActivity appCompatActivity = (AppCompatActivity) requireActivity(); - actionBar = appCompatActivity.getSupportActionBar(); - } - private void setupVideoView() { - initWindowInsetsController(); - initActionBar(); - - int type = WindowInsetsCompat.Type.systemBars(); - binding.exoplayerView.setFullscreenButtonClickListener(isFullScreen -> { - Log_OC.e(TAG, "Fullscreen: " + isFullScreen); - if (isFullScreen) { - windowInsetsController.hide(type); - actionBar.hide(); - } else { - windowInsetsController.show(type); - actionBar.show(); - } - }); binding.exoplayerView.setPlayer(exoPlayer); + binding.exoplayerView.setFullscreenButtonClickListener(isFullScreen -> startFullScreenVideo()); } private void stopAudio() { @@ -636,10 +606,6 @@ public class PreviewMediaFragment extends FileFragment implements public void onDestroyView() { Log_OC.v(TAG, "onDestroyView"); super.onDestroyView(); - if (windowInsetsController != null && actionBar != null) { - windowInsetsController.show(WindowInsetsCompat.Type.systemBars()); - actionBar.show(); - } binding = null; } @@ -659,6 +625,25 @@ public class PreviewMediaFragment extends FileFragment implements super.onStop(); } + @Override + public boolean onTouch(View v, MotionEvent event) { + if (event.getAction() == MotionEvent.ACTION_DOWN && v.equals(binding.exoplayerView)) { + // added a margin on the left to avoid interfering with gesture to open navigation drawer + if (event.getX() / Resources.getSystem().getDisplayMetrics().density > MIN_DENSITY_RATIO) { + startFullScreenVideo(); + } + return true; + } + return false; + } + + private void startFullScreenVideo() { + final FragmentActivity activity = getActivity(); + if (activity != null) { + new PreviewVideoFullscreenDialog(activity, nextcloudClient, exoPlayer, binding.exoplayerView).show(); + } + } + @Override public void onConfigurationChanged(@NonNull Configuration newConfig) { super.onConfigurationChanged(newConfig); diff --git a/app/src/main/java/com/owncloud/android/ui/preview/PreviewVideoFullscreenDialog.kt b/app/src/main/java/com/owncloud/android/ui/preview/PreviewVideoFullscreenDialog.kt new file mode 100644 index 0000000000..99615f20f6 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/preview/PreviewVideoFullscreenDialog.kt @@ -0,0 +1,184 @@ +/* + * 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 . + * + */ + +package com.owncloud.android.ui.preview + +import android.app.Activity +import android.app.Dialog +import android.os.Build +import android.view.ViewGroup +import android.view.Window +import androidx.annotation.OptIn +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsControllerCompat +import androidx.media3.common.util.UnstableApi +import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.ui.PlayerView +import com.nextcloud.client.media.ExoplayerListener +import com.nextcloud.client.media.NextcloudExoPlayer +import com.nextcloud.common.NextcloudClient +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 + */ +@OptIn(UnstableApi::class) +class PreviewVideoFullscreenDialog( + private val activity: Activity, + nextcloudClient: NextcloudClient, + private val sourceExoPlayer: ExoPlayer, + private val sourceView: PlayerView +) : Dialog(sourceView.context, android.R.style.Theme_Black_NoTitleBar_Fullscreen) { + + private val binding: DialogPreviewVideoBinding = DialogPreviewVideoBinding.inflate(layoutInflater) + private var playingStateListener: androidx.media3.common.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() + } + setOnShowListener { + enableImmersiveMode() + switchTargetViewFromSource() + binding.videoPlayer.setFullscreenButtonClickListener { onBackPressed() } + if (isPlaying) { + mExoPlayer.play() + } + } + super.show() + } + + private fun switchTargetViewFromSource() { + if (shouldUseRotatedVideoWorkaround) { + mExoPlayer.seekTo(sourceExoPlayer.currentPosition) + } else { + PlayerView.switchTargetView(sourceExoPlayer, sourceView, binding.videoPlayer) + } + } + + + override fun onBackPressed() { + val isPlaying = mExoPlayer.isPlaying + if (isPlaying) { + mExoPlayer.pause() + } + setOnDismissListener { + disableImmersiveMode() + playingStateListener?.let { + mExoPlayer.removeListener(it) + } + switchTargetViewToSource() + if (isPlaying) { + sourceExoPlayer.play() + } + sourceView.showController() + } + dismiss() + } + + private fun switchTargetViewToSource() { + if (shouldUseRotatedVideoWorkaround) { + sourceExoPlayer.seekTo(mExoPlayer.currentPosition) + } else { + PlayerView.switchTargetView(sourceExoPlayer, binding.videoPlayer, sourceView) + } + } + + private fun enableImmersiveMode() { + activity.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 + } +} diff --git a/app/src/main/res/layout/dialog_preview_video.xml b/app/src/main/res/layout/dialog_preview_video.xml new file mode 100644 index 0000000000..cbae3164dc --- /dev/null +++ b/app/src/main/res/layout/dialog_preview_video.xml @@ -0,0 +1,28 @@ + +