mirror of
https://github.com/nextcloud/android.git
synced 2024-11-23 21:55:48 +03:00
remove dependency on internal player layout
Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>
This commit is contained in:
parent
40bb49c69f
commit
80d52f5ef6
3 changed files with 237 additions and 40 deletions
|
@ -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);
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
28
app/src/main/res/layout/dialog_preview_video.xml
Normal file
28
app/src/main/res/layout/dialog_preview_video.xml
Normal 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" />
|
Loading…
Reference in a new issue