remove dependency on internal player layout

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>
This commit is contained in:
parneet-guraya 2023-12-14 19:03:13 +05:30
parent 40bb49c69f
commit 80d52f5ef6
No known key found for this signature in database
GPG key ID: 26DB680F1EE174D5
3 changed files with 237 additions and 40 deletions

View file

@ -31,6 +31,7 @@ import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
@ -44,7 +45,9 @@ import android.view.LayoutInflater;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View; import android.view.View;
import android.view.View.OnTouchListener;
import android.view.ViewGroup; import android.view.ViewGroup;
import com.nextcloud.client.account.User; import com.nextcloud.client.account.User;
@ -82,22 +85,15 @@ import java.util.concurrent.Executors;
import javax.inject.Inject; import javax.inject.Inject;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.OptIn;
import androidx.annotation.StringRes; import androidx.annotation.StringRes;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.content.res.AppCompatResources; import androidx.appcompat.content.res.AppCompatResources;
import androidx.core.graphics.drawable.DrawableCompat; 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.drawerlayout.widget.DrawerLayout;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentManager;
import androidx.media3.common.MediaItem; import androidx.media3.common.MediaItem;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.exoplayer.ExoPlayer; import androidx.media3.exoplayer.ExoPlayer;
@OptIn(markerClass = UnstableApi.class)
/** /**
* This fragment shows a preview of a downloaded media file (audio or video). * This fragment shows a preview of a downloaded media file (audio or video).
* <p> * <p>
@ -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 * By now, if the {@link OCFile} passed is not downloaded, an {@link IllegalStateException} is generated on
* instantiation too. * instantiation too.
*/ */
public class PreviewMediaFragment extends FileFragment implements public class PreviewMediaFragment extends FileFragment implements OnTouchListener,
Injectable { Injectable {
private static final String TAG = PreviewMediaFragment.class.getSimpleName(); private static final String TAG = PreviewMediaFragment.class.getSimpleName();
@ -144,8 +140,6 @@ public class PreviewMediaFragment extends FileFragment implements
private ViewGroup emptyListView; private ViewGroup emptyListView;
private ExoPlayer exoPlayer; private ExoPlayer exoPlayer;
private NextcloudClient nextcloudClient; private NextcloudClient nextcloudClient;
private WindowInsetsControllerCompat windowInsetsController;
private ActionBar actionBar;
/** /**
* Creates a fragment to preview a file. * Creates a fragment to preview a file.
@ -260,7 +254,6 @@ public class PreviewMediaFragment extends FileFragment implements
if (MimeTypeUtil.isVideo(file)) { if (MimeTypeUtil.isVideo(file)) {
binding.exoplayerView.setVisibility(View.VISIBLE); binding.exoplayerView.setVisibility(View.VISIBLE);
binding.imagePreview.setVisibility(View.GONE); binding.imagePreview.setVisibility(View.GONE);
binding.getRoot().setBackgroundColor(getResources().getColor(R.color.black, null));
} else { } else {
binding.exoplayerView.setVisibility(View.GONE); binding.exoplayerView.setVisibility(View.GONE);
binding.imagePreview.setVisibility(View.VISIBLE); 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() { 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.setPlayer(exoPlayer);
binding.exoplayerView.setFullscreenButtonClickListener(isFullScreen -> startFullScreenVideo());
} }
private void stopAudio() { private void stopAudio() {
@ -636,10 +606,6 @@ public class PreviewMediaFragment extends FileFragment implements
public void onDestroyView() { public void onDestroyView() {
Log_OC.v(TAG, "onDestroyView"); Log_OC.v(TAG, "onDestroyView");
super.onDestroyView(); super.onDestroyView();
if (windowInsetsController != null && actionBar != null) {
windowInsetsController.show(WindowInsetsCompat.Type.systemBars());
actionBar.show();
}
binding = null; binding = null;
} }
@ -659,6 +625,25 @@ public class PreviewMediaFragment extends FileFragment implements
super.onStop(); 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 @Override
public void onConfigurationChanged(@NonNull Configuration newConfig) { public void onConfigurationChanged(@NonNull Configuration newConfig) {
super.onConfigurationChanged(newConfig); super.onConfigurationChanged(newConfig);

View file

@ -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 <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.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
}
}

View file

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?><!--
Nextcloud Android client application
@author Tobias Kaminsky
Copyright (C) 2021 Tobias Kaminsky
Copyright (C) 2021 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 as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) 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 <https://www.gnu.org/licenses/>.
-->
<androidx.media3.ui.PlayerView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/videoPlayer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:background="@color/black"
app:show_buffering="always" />