Browse Source

remove dependency on internal player layout

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>
parneet-guraya 1 year ago
parent
commit
80d52f5ef6

+ 25 - 40
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).
  * <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
  * 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);

+ 184 - 0
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 <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 - 0
app/src/main/res/layout/dialog_preview_video.xml

@@ -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" />