Procházet zdrojové kódy

Merge pull request #11936 from nextcloud/migrate-to-media3

Migrate to media3 and Immersive mode for video playback
Alper Öztürk před 1 rokem
rodič
revize
76369d4880

+ 3 - 2
app/build.gradle

@@ -299,8 +299,9 @@ dependencies {
 
     implementation 'org.conscrypt:conscrypt-android:2.5.2'
 
-    implementation "com.google.android.exoplayer:exoplayer:$exoplayerVersion"
-    implementation "com.google.android.exoplayer:extension-okhttp:$exoplayerVersion"
+    implementation "androidx.media3:media3-ui:1.2.0"
+    implementation "androidx.media3:media3-exoplayer:1.2.0"
+    implementation "androidx.media3:media3-datasource-okhttp:1.2.0"
 
     implementation 'me.zhanghai.android.fastscroll:library:1.2.0'
 

+ 5 - 0
app/src/main/AndroidManifest.xml

@@ -234,6 +234,11 @@
             android:name=".ui.preview.PreviewImageActivity"
             android:exported="false"
             android:theme="@style/Theme.ownCloud.Overlay" />
+        <activity
+            android:name=".ui.preview.PreviewMediaActivity"
+            android:exported="false"
+            android:configChanges="orientation|screenLayout|screenSize|keyboardHidden"
+            android:theme="@style/Theme.ownCloud.Media" />
         <service
             android:name=".authentication.AccountAuthenticatorService"
             android:exported="false">

+ 4 - 0
app/src/main/java/com/nextcloud/client/di/ComponentsModule.java

@@ -127,6 +127,7 @@ import com.owncloud.android.ui.preview.FileDownloadFragment;
 import com.owncloud.android.ui.preview.PreviewBitmapActivity;
 import com.owncloud.android.ui.preview.PreviewImageActivity;
 import com.owncloud.android.ui.preview.PreviewImageFragment;
+import com.owncloud.android.ui.preview.PreviewMediaActivity;
 import com.owncloud.android.ui.preview.PreviewMediaFragment;
 import com.owncloud.android.ui.preview.PreviewTextFileFragment;
 import com.owncloud.android.ui.preview.PreviewTextFragment;
@@ -206,6 +207,9 @@ abstract class ComponentsModule {
     @ContributesAndroidInjector
     abstract PreviewImageActivity previewImageActivity();
 
+    @ContributesAndroidInjector
+    abstract PreviewMediaActivity previewMediaActivity();
+
     @ContributesAndroidInjector
     abstract ReceiveExternalFilesActivity receiveExternalFilesActivity();
 

+ 1 - 1
app/src/main/java/com/nextcloud/client/media/ErrorFormat.kt

@@ -25,7 +25,7 @@ package com.nextcloud.client.media
 
 import android.content.Context
 import android.media.MediaPlayer
-import com.google.android.exoplayer2.PlaybackException
+import androidx.media3.common.PlaybackException
 import com.owncloud.android.R
 
 /**

+ 3 - 3
app/src/main/java/com/nextcloud/client/media/ExoplayerListener.kt

@@ -25,9 +25,9 @@ package com.nextcloud.client.media
 import android.content.Context
 import android.content.DialogInterface
 import android.view.View
-import com.google.android.exoplayer2.ExoPlayer
-import com.google.android.exoplayer2.PlaybackException
-import com.google.android.exoplayer2.Player
+import androidx.media3.common.PlaybackException
+import androidx.media3.common.Player
+import androidx.media3.exoplayer.ExoPlayer
 import com.google.android.material.dialog.MaterialAlertDialogBuilder
 import com.owncloud.android.R
 import com.owncloud.android.lib.common.utils.Log_OC

+ 7 - 4
app/src/main/java/com/nextcloud/client/media/NextcloudExoPlayer.kt

@@ -22,10 +22,12 @@
 package com.nextcloud.client.media
 
 import android.content.Context
-import com.google.android.exoplayer2.ExoPlayer
-import com.google.android.exoplayer2.ext.okhttp.OkHttpDataSource
-import com.google.android.exoplayer2.source.DefaultMediaSourceFactory
-import com.google.android.exoplayer2.upstream.DefaultDataSource
+import androidx.annotation.OptIn
+import androidx.media3.common.util.UnstableApi
+import androidx.media3.datasource.DefaultDataSource
+import androidx.media3.datasource.okhttp.OkHttpDataSource
+import androidx.media3.exoplayer.ExoPlayer
+import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
 import com.nextcloud.common.NextcloudClient
 import com.owncloud.android.MainApp
 
@@ -37,6 +39,7 @@ object NextcloudExoPlayer {
      * IP versions and certificates.
      *
      */
+    @OptIn(UnstableApi::class)
     @JvmStatic
     fun createNextcloudExoplayer(context: Context, nextcloudClient: NextcloudClient): ExoPlayer {
         val okHttpDataSourceFactory = OkHttpDataSource.Factory(nextcloudClient.client)

+ 28 - 19
app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java

@@ -123,6 +123,7 @@ import com.owncloud.android.ui.helpers.FileOperationsHelper;
 import com.owncloud.android.ui.helpers.UriUploader;
 import com.owncloud.android.ui.preview.PreviewImageActivity;
 import com.owncloud.android.ui.preview.PreviewImageFragment;
+import com.owncloud.android.ui.preview.PreviewMediaActivity;
 import com.owncloud.android.ui.preview.PreviewMediaFragment;
 import com.owncloud.android.ui.preview.PreviewTextFileFragment;
 import com.owncloud.android.ui.preview.PreviewTextFragment;
@@ -168,7 +169,10 @@ import static com.owncloud.android.utils.PermissionUtil.PERMISSION_CHOICE_DIALOG
 /**
  * Displays, what files the user has available in his ownCloud. This is the main view.
  */
-public class FileDisplayActivity extends FileActivity implements FileFragment.ContainerActivity, OnEnforceableRefreshListener, SortingOrderDialogFragment.OnSortingOrderListener, SendShareDialog.SendShareDialogDownloader, Injectable {
+public class FileDisplayActivity extends FileActivity
+    implements FileFragment.ContainerActivity,
+    OnEnforceableRefreshListener, SortingOrderDialogFragment.OnSortingOrderListener,
+    SendShareDialog.SendShareDialogDownloader, Injectable {
 
     public static final String RESTART = "RESTART";
     public static final String ALL_FILES = "ALL_FILES";
@@ -693,8 +697,8 @@ public class FileDisplayActivity extends FileActivity implements FileFragment.Co
                         // update the file from database, for the local storage path
                         mWaitingToPreview = getStorageManager().getFileById(mWaitingToPreview.getFileId());
 
-                        if (PreviewMediaFragment.canBePreviewed(mWaitingToPreview)) {
-                            startMediaPreview(mWaitingToPreview, 0, true, true, true);
+                        if (PreviewMediaActivity.Companion.canBePreviewed(mWaitingToPreview)) {
+                            startMediaPreview(mWaitingToPreview, 0, true, true, true, true);
                             detailsFragmentChanged = true;
                         } else if (MimeTypeUtil.isVCard(mWaitingToPreview.getMimeType())) {
                             startContactListFragment(mWaitingToPreview);
@@ -1391,7 +1395,7 @@ public class FileDisplayActivity extends FileActivity implements FileFragment.Co
                 if (uploadWasFine) {
                     OCFile ocFile = getFile();
                     if (PreviewImageFragment.canBePreviewed(ocFile)) {
-                        startImagePreview(getFile(),true);
+                        startImagePreview(getFile(), true);
                     } else if (PreviewTextFileFragment.canBePreviewed(ocFile)) {
                         startTextPreview(ocFile, true);
                     }
@@ -1643,10 +1647,7 @@ public class FileDisplayActivity extends FileActivity implements FileFragment.Co
             OCFile file = ((FileFragment) details).getFile();
             if (file != null) {
                 file = getStorageManager().getFileByPath(file.getRemotePath());
-                if (details instanceof PreviewMediaFragment) {
-                    // Refresh  OCFile of the fragment
-                    ((PreviewMediaFragment) details).updateFile(file);
-                } else if (details instanceof PreviewTextFragment) {
+                if (details instanceof PreviewTextFragment) {
                     // Refresh  OCFile of the fragment
                     ((PreviewTextFileFragment) details).updateFile(file);
                 } else {
@@ -1676,12 +1677,7 @@ public class FileDisplayActivity extends FileActivity implements FileFragment.Co
 
             // check if file is still available, if so do nothing
             boolean fileAvailable = getStorageManager().fileExists(removedFile.getFileId());
-
             if (leftFragment instanceof FileFragment && !fileAvailable && removedFile.equals(((FileFragment) leftFragment).getFile())) {
-                if (leftFragment instanceof PreviewMediaFragment previewMediaFragment) {
-                    previewMediaFragment.stopPreview(true);
-                    onBackPressed();
-                }
                 setFile(getStorageManager().getFileById(removedFile.getParentId()));
                 resetTitleBarAndScrolling();
             }
@@ -1796,7 +1792,7 @@ public class FileDisplayActivity extends FileActivity implements FileFragment.Co
                     ((PreviewMediaFragment) fileFragment).updateFile(renamedFile);
                     if (PreviewMediaFragment.canBePreviewed(renamedFile)) {
                         long position = ((PreviewMediaFragment) fileFragment).getPosition();
-                        startMediaPreview(renamedFile, position, true, true, true);
+                        startMediaPreview(renamedFile, position, true, true, true, false);
                     } else {
                         getFileOperationsHelper().openFile(renamedFile);
                     }
@@ -2041,15 +2037,19 @@ public class FileDisplayActivity extends FileActivity implements FileFragment.Co
      * @param startPlaybackPosition Media position where the playback will be started, in milliseconds.
      * @param autoplay              When 'true', the playback will start without user interactions.
      */
-    public void startMediaPreview(OCFile file, long startPlaybackPosition, boolean autoplay, boolean showPreview, boolean streamMedia) {
+    public void startMediaPreview(OCFile file, long startPlaybackPosition, boolean autoplay, boolean showPreview, boolean streamMedia, boolean showInActivity) {
         Optional<User> user = getUser();
         if (!user.isPresent()) {
             return; // not reachable under normal conditions
         }
         if (showPreview && file.isDown() && !file.isDownloading() || streamMedia) {
-            configureToolbarForPreview(file);
-            Fragment mediaFragment = PreviewMediaFragment.newInstance(file, user.get(), startPlaybackPosition, autoplay, false);
-            setLeftFragment(mediaFragment, false);
+            if (showInActivity) {
+                startMediaActivity(file, startPlaybackPosition, autoplay, user);
+            } else {
+                configureToolbarForPreview(file);
+                Fragment mediaFragment = PreviewMediaFragment.newInstance(file, user.get(), startPlaybackPosition, autoplay, false);
+                setLeftFragment(mediaFragment, false);
+            }
         } else {
             Intent previewIntent = new Intent();
             previewIntent.putExtra(EXTRA_FILE, file);
@@ -2060,6 +2060,15 @@ public class FileDisplayActivity extends FileActivity implements FileFragment.Co
         }
     }
 
+    private void startMediaActivity(OCFile file, long startPlaybackPosition, boolean autoplay, Optional<User> user) {
+        Intent previewMediaIntent = new Intent(this, PreviewMediaActivity.class);
+        previewMediaIntent.putExtra(PreviewMediaActivity.EXTRA_FILE, file);
+        previewMediaIntent.putExtra(PreviewMediaActivity.EXTRA_USER, user.get());
+        previewMediaIntent.putExtra(PreviewMediaActivity.EXTRA_START_POSITION, startPlaybackPosition);
+        previewMediaIntent.putExtra(PreviewMediaActivity.EXTRA_AUTOPLAY, autoplay);
+        startActivity(previewMediaIntent);
+    }
+
     public void configureToolbarForPreview(OCFile file) {
         lockScrolling();
         super.updateActionBarTitleAndHomeButton(file);
@@ -2242,7 +2251,7 @@ public class FileDisplayActivity extends FileActivity implements FileFragment.Co
         if (event.getIntent().getBooleanExtra(TEXT_PREVIEW, false)) {
             startTextPreview((OCFile) bundle.get(EXTRA_FILE), true);
         } else if (bundle.containsKey(PreviewMediaFragment.EXTRA_START_POSITION)) {
-            startMediaPreview((OCFile) bundle.get(EXTRA_FILE), (long) bundle.get(PreviewMediaFragment.EXTRA_START_POSITION), (boolean) bundle.get(PreviewMediaFragment.EXTRA_AUTOPLAY), true, true);
+            startMediaPreview((OCFile) bundle.get(EXTRA_FILE), (long) bundle.get(PreviewMediaFragment.EXTRA_START_POSITION), (boolean) bundle.get(PreviewMediaFragment.EXTRA_AUTOPLAY), true, true, true);
         } else if (bundle.containsKey(PreviewImageActivity.EXTRA_VIRTUAL_TYPE)) {
             startImagePreview((OCFile) bundle.get(EXTRA_FILE), (VirtualFolderType) bundle.get(PreviewImageActivity.EXTRA_VIRTUAL_TYPE), true);
         } else {

+ 3 - 1
app/src/main/java/com/owncloud/android/ui/dialog/SetupEncryptionDialogFragment.kt

@@ -155,6 +155,7 @@ class SetupEncryptionDialogFragment : DialogFragment(), Injectable {
                 dialog.dismiss()
                 notifyResult()
             }
+
             KEY_EXISTING_USED -> {
                 decryptPrivateKey(dialog)
             }
@@ -162,6 +163,7 @@ class SetupEncryptionDialogFragment : DialogFragment(), Injectable {
             KEY_GENERATE -> {
                 generateKey()
             }
+
             else -> dialog.dismiss()
         }
     }
@@ -376,7 +378,7 @@ class SetupEncryptionDialogFragment : DialogFragment(), Injectable {
             binding.encryptionStatus.setText(R.string.end_to_end_encryption_generating_keys)
         }
 
-        @Suppress("TooGenericExceptionCaught", "TooGenericExceptionThrown", "ReturnCount")
+        @Suppress("TooGenericExceptionCaught", "TooGenericExceptionThrown", "ReturnCount", "LongMethod")
         @Deprecated("Deprecated in Java")
         override fun doInBackground(vararg voids: Void?): String {
             //  - create CSR, push to server, store returned public key in database

+ 10 - 8
app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java

@@ -112,7 +112,7 @@ import com.owncloud.android.ui.events.SearchEvent;
 import com.owncloud.android.ui.helpers.FileOperationsHelper;
 import com.owncloud.android.ui.interfaces.OCFileListFragmentInterface;
 import com.owncloud.android.ui.preview.PreviewImageFragment;
-import com.owncloud.android.ui.preview.PreviewMediaFragment;
+import com.owncloud.android.ui.preview.PreviewMediaActivity;
 import com.owncloud.android.ui.preview.PreviewTextFileFragment;
 import com.owncloud.android.utils.DisplayUtils;
 import com.owncloud.android.utils.EncryptionUtils;
@@ -139,6 +139,7 @@ import javax.inject.Inject;
 import androidx.annotation.IdRes;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
 import androidx.annotation.StringRes;
 import androidx.appcompat.app.ActionBar;
 import androidx.coordinatorlayout.widget.CoordinatorLayout;
@@ -146,6 +147,7 @@ import androidx.core.content.ContextCompat;
 import androidx.drawerlayout.widget.DrawerLayout;
 import androidx.fragment.app.FragmentActivity;
 import androidx.fragment.app.FragmentManager;
+import androidx.media3.common.util.UnstableApi;
 import androidx.recyclerview.widget.GridLayoutManager;
 import androidx.recyclerview.widget.LinearLayoutManager;
 import androidx.recyclerview.widget.RecyclerView;
@@ -977,6 +979,7 @@ public class OCFileListFragment extends ExtendedListFragment implements
     }
 
     @Override
+    @OptIn(markerClass = UnstableApi.class)
     public void onItemClicked(OCFile file) {
         ((FileActivity) mContainerActivity).checkInternetConnection();
 
@@ -1068,10 +1071,10 @@ public class OCFileListFragment extends ExtendedListFragment implements
                         setFabVisible(false);
                         ((FileDisplayActivity) mContainerActivity).startTextPreview(file, false);
                     } else if (file.isDown()) {
-                        if (PreviewMediaFragment.canBePreviewed(file)) {
+                        if (PreviewMediaActivity.Companion.canBePreviewed(file)) {
                             // media preview
                             setFabVisible(false);
-                            ((FileDisplayActivity) mContainerActivity).startMediaPreview(file, 0, true, true, false);
+                            ((FileDisplayActivity) mContainerActivity).startMediaPreview(file, 0, true, true, false, true);
                         } else {
                             mContainerActivity.getFileOperationsHelper().openFile(file);
                         }
@@ -1081,10 +1084,10 @@ public class OCFileListFragment extends ExtendedListFragment implements
                         OCCapability capability = mContainerActivity.getStorageManager()
                             .getCapability(account.getAccountName());
 
-                        if (PreviewMediaFragment.canBePreviewed(file) && !file.isEncrypted()) {
+                        if (PreviewMediaActivity.Companion.canBePreviewed(file) && !file.isEncrypted()) {
                             // stream media preview on >= NC14
                             setFabVisible(false);
-                            ((FileDisplayActivity) mContainerActivity).startMediaPreview(file, 0, true, true, true);
+                            ((FileDisplayActivity) mContainerActivity).startMediaPreview(file, 0, true, true, true, true);
                         } else if (editorUtils.isEditorAvailable(accountManager.getUser(),
                                                                  file.getMimeType()) &&
                             !file.isEncrypted()) {
@@ -1822,8 +1825,7 @@ public class OCFileListFragment extends ExtendedListFragment implements
     }
 
     /**
-     * Theme default action bar according to provided parameters.
-     * Replaces back arrow with hamburger menu icon.
+     * Theme default action bar according to provided parameters. Replaces back arrow with hamburger menu icon.
      *
      * @param title string res id of title to be shown in action bar
      */
@@ -1834,7 +1836,7 @@ public class OCFileListFragment extends ExtendedListFragment implements
     /**
      * Theme default action bar according to provided parameters.
      *
-     * @param title title to be shown in action bar
+     * @param title          title to be shown in action bar
      * @param showBackAsMenu iff true replace back arrow with hamburger menu icon
      */
     protected void setTitle(final String title, Boolean showBackAsMenu) {

+ 12 - 8
app/src/main/java/com/owncloud/android/ui/preview/PreviewImagePagerAdapter.java

@@ -19,6 +19,7 @@
  */
 package com.owncloud.android.ui.preview;
 
+import android.content.Intent;
 import android.util.SparseArray;
 import android.view.ViewGroup;
 
@@ -38,9 +39,11 @@ import java.util.Set;
 import javax.annotation.Nullable;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.OptIn;
 import androidx.fragment.app.Fragment;
 import androidx.fragment.app.FragmentManager;
 import androidx.fragment.app.FragmentStatePagerAdapter;
+import androidx.media3.common.util.UnstableApi;
 
 /**
  * Adapter class that provides Fragment instances
@@ -59,10 +62,10 @@ public class PreviewImagePagerAdapter extends FragmentStatePagerAdapter {
     /**
      * Constructor
      *
-     * @param fragmentManager   {@link FragmentManager} instance that will handle
-     *                          the {@link Fragment}s provided by the adapter.
-     * @param parentFolder      Folder where images will be searched for.
-     * @param storageManager    Bridge to database.
+     * @param fragmentManager {@link FragmentManager} instance that will handle the {@link Fragment}s provided by the
+     *                        adapter.
+     * @param parentFolder    Folder where images will be searched for.
+     * @param storageManager  Bridge to database.
      */
     public PreviewImagePagerAdapter(FragmentManager fragmentManager,
                                     OCFile selectedFile,
@@ -96,8 +99,8 @@ public class PreviewImagePagerAdapter extends FragmentStatePagerAdapter {
     /**
      * Constructor
      *
-     * @param fragmentManager {@link FragmentManager} instance that will handle
-     *                        the {@link Fragment}s provided by the adapter.
+     * @param fragmentManager {@link FragmentManager} instance that will handle the {@link Fragment}s provided by the
+     *                        adapter.
      * @param type            Type of virtual folder, e.g. favorite or photos
      * @param storageManager  Bridge to database.
      */
@@ -113,7 +116,7 @@ public class PreviewImagePagerAdapter extends FragmentStatePagerAdapter {
         if (type == null) {
             throw new IllegalArgumentException("NULL parent folder");
         }
-        if(type == VirtualFolderType.NONE){
+        if (type == VirtualFolderType.NONE) {
             throw new IllegalArgumentException("NONE virtual folder type");
         }
         if (storageManager == null) {
@@ -155,6 +158,7 @@ public class PreviewImagePagerAdapter extends FragmentStatePagerAdapter {
     }
 
     @NonNull
+    @OptIn(markerClass = UnstableApi.class)
     public Fragment getItem(int i) {
         OCFile file = getFileAt(i);
         Fragment fragment;
@@ -242,7 +246,7 @@ public class PreviewImagePagerAdapter extends FragmentStatePagerAdapter {
     @Override
     public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
         mCachedFragments.remove(position);
-       super.destroyItem(container, position, object);
+        super.destroyItem(container, position, object);
     }
 
 

+ 834 - 0
app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaActivity.kt

@@ -0,0 +1,834 @@
+/*
+ *   ownCloud Android client application
+ *
+ *   @author David A. Velasco
+ *   @author Chris Narkiewicz
+ *   @author Andy Scherzinger
+ *   @author TSI-mc
+ *   @author Parneet Singh
+ *   Copyright (C) 2016 ownCloud Inc.
+ *   Copyright (C) 2019 Chris Narkiewicz <hello@ezaquarii.com>
+ *   Copyright (C) 2020 Andy Scherzinger
+ *   Copyright (C) 2023 TSI-mc
+ *   Copyright (C) Parneet Singh
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License version 2,
+ *   as published by the Free Software Foundation.
+ *
+ *   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 General Public License for more details.
+ *
+ *   You should have received a copy of the GNU 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.content.ComponentName
+import android.content.Intent
+import android.content.ServiceConnection
+import android.content.res.Configuration
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.graphics.drawable.Drawable
+import android.media.MediaMetadataRetriever
+import android.net.Uri
+import android.os.AsyncTask
+import android.os.Bundle
+import android.os.Handler
+import android.os.IBinder
+import android.os.Looper
+import android.view.Menu
+import android.view.MenuItem
+import android.view.View
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import androidx.annotation.OptIn
+import androidx.annotation.StringRes
+import androidx.appcompat.content.res.AppCompatResources
+import androidx.core.graphics.drawable.DrawableCompat
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.WindowInsetsControllerCompat
+import androidx.core.view.marginBottom
+import androidx.core.view.updateLayoutParams
+import androidx.core.view.updatePadding
+import androidx.media3.common.MediaItem
+import androidx.media3.common.util.UnstableApi
+import androidx.media3.exoplayer.ExoPlayer
+import androidx.media3.ui.DefaultTimeBar
+import androidx.media3.ui.PlayerView
+import com.nextcloud.client.account.User
+import com.nextcloud.client.account.UserAccountManager
+import com.nextcloud.client.di.Injectable
+import com.nextcloud.client.jobs.BackgroundJobManager
+import com.nextcloud.client.media.ExoplayerListener
+import com.nextcloud.client.media.NextcloudExoPlayer.createNextcloudExoplayer
+import com.nextcloud.client.media.PlayerServiceConnection
+import com.nextcloud.client.network.ClientFactory
+import com.nextcloud.client.network.ClientFactory.CreationException
+import com.nextcloud.common.NextcloudClient
+import com.nextcloud.ui.fileactions.FileActionsBottomSheet.Companion.newInstance
+import com.nextcloud.ui.fileactions.FileActionsBottomSheet.ResultListener
+import com.nextcloud.utils.extensions.getParcelableArgument
+import com.owncloud.android.R
+import com.owncloud.android.databinding.ActivityPreviewMediaBinding
+import com.owncloud.android.datamodel.OCFile
+import com.owncloud.android.datamodel.ThumbnailsCacheManager
+import com.owncloud.android.files.StreamMediaFileOperation
+import com.owncloud.android.files.services.FileDownloader
+import com.owncloud.android.files.services.FileDownloader.FileDownloaderBinder
+import com.owncloud.android.lib.common.OwnCloudClient
+import com.owncloud.android.lib.common.operations.OnRemoteOperationListener
+import com.owncloud.android.lib.common.operations.RemoteOperation
+import com.owncloud.android.lib.common.operations.RemoteOperationResult
+import com.owncloud.android.lib.common.utils.Log_OC
+import com.owncloud.android.operations.RemoveFileOperation
+import com.owncloud.android.operations.SynchronizeFileOperation
+import com.owncloud.android.ui.activity.FileActivity
+import com.owncloud.android.ui.activity.FileDisplayActivity
+import com.owncloud.android.ui.dialog.ConfirmationDialogFragment
+import com.owncloud.android.ui.dialog.RemoveFilesDialogFragment
+import com.owncloud.android.ui.dialog.SendShareDialog
+import com.owncloud.android.ui.fragment.FileFragment
+import com.owncloud.android.ui.fragment.OCFileListFragment
+import com.owncloud.android.utils.BitmapUtils
+import com.owncloud.android.utils.DisplayUtils
+import com.owncloud.android.utils.ErrorMessageAdapter
+import com.owncloud.android.utils.MimeTypeUtil
+import java.lang.ref.WeakReference
+import java.util.concurrent.Executors
+import javax.inject.Inject
+
+/**
+ * This activity shows a preview of a downloaded media file (audio or video).
+ *
+ *
+ * Trying to get an instance with NULL [OCFile] or ownCloud [User] values will produce an [ ].
+ *
+ *
+ * By now, if the [OCFile] passed is not downloaded, an [IllegalStateException] is generated on
+ * instantiation too.
+ */
+@Suppress("TooManyFunctions")
+class PreviewMediaActivity :
+    FileActivity(),
+    FileFragment.ContainerActivity,
+    OnRemoteOperationListener,
+    SendShareDialog.SendShareDialogDownloader,
+    Injectable {
+
+    private var user: User? = null
+    private var savedPlaybackPosition: Long = 0
+    private var autoplay = true
+    private val prepared = false
+    private var mediaPlayerServiceConnection: PlayerServiceConnection? = null
+    private var videoUri: Uri? = null
+
+    @Inject
+    lateinit var clientFactory: ClientFactory
+
+    @Inject
+    lateinit var accountManager: UserAccountManager
+
+    @Inject
+    lateinit var backgroundJobManager: BackgroundJobManager
+
+    private lateinit var binding: ActivityPreviewMediaBinding
+    private var emptyListView: ViewGroup? = null
+    private var exoPlayer: ExoPlayer? = null
+    private var nextcloudClient: NextcloudClient? = null
+    private lateinit var windowInsetsController: WindowInsetsControllerCompat
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        binding = ActivityPreviewMediaBinding.inflate(layoutInflater)
+        setContentView(binding.root)
+
+        setSupportActionBar(binding.materialToolbar)
+        WindowCompat.setDecorFitsSystemWindows(window, false)
+        applyWindowInsets()
+        initArguments(savedInstanceState)
+        mediaPlayerServiceConnection = PlayerServiceConnection(this)
+        showMediaTypeViews()
+        configureSystemBars()
+        emptyListView = binding.emptyView.emptyListView
+        setLoadingView()
+    }
+
+    private fun initArguments(savedInstanceState: Bundle?) {
+        intent?.let {
+            initWithIntent(it)
+        }
+
+        if (savedInstanceState == null) {
+            checkNotNull(file) { "Instanced with a NULL OCFile" }
+            checkNotNull(user) { "Instanced with a NULL ownCloud Account" }
+        } else {
+            initWithBundle(savedInstanceState)
+        }
+    }
+
+    private fun initWithIntent(intent: Intent) {
+        file = intent.getParcelableArgument(FILE, OCFile::class.java)
+        user = intent.getParcelableArgument(USER, User::class.java)
+        savedPlaybackPosition = intent.getLongExtra(PLAYBACK_POSITION, 0L)
+        autoplay = intent.getBooleanExtra(AUTOPLAY, true)
+    }
+
+    private fun initWithBundle(bundle: Bundle) {
+        file = bundle.getParcelableArgument(EXTRA_FILE, OCFile::class.java)
+        user = bundle.getParcelableArgument(EXTRA_USER, User::class.java)
+        savedPlaybackPosition = bundle.getInt(EXTRA_PLAY_POSITION).toLong()
+        autoplay = bundle.getBoolean(EXTRA_PLAYING)
+    }
+
+    private fun showMediaTypeViews() {
+        if (file == null) {
+            return
+        }
+
+        val isFileVideo = MimeTypeUtil.isVideo(file)
+
+        binding.exoplayerView.visibility = if (isFileVideo) View.VISIBLE else View.GONE
+        binding.imagePreview.visibility = if (isFileVideo) View.GONE else View.VISIBLE
+
+        if (isFileVideo) {
+            binding.root.setBackgroundColor(resources.getColor(R.color.black, null))
+        } else {
+            extractAndSetCoverArt(file)
+        }
+    }
+
+    private fun configureSystemBars() {
+        updateActionBarTitleAndHomeButton(file)
+
+        supportActionBar?.let {
+            it.setDisplayHomeAsUpEnabled(true)
+            viewThemeUtils.files.themeActionBar(this, it)
+        }
+
+        viewThemeUtils.platform.themeStatusBar(
+            this
+        )
+    }
+
+    private fun setLoadingView() {
+        binding.progress.visibility = View.VISIBLE
+        binding.emptyView.emptyListView.visibility = View.GONE
+    }
+
+    private fun setVideoErrorMessage(headline: String, @StringRes message: Int) {
+        binding.emptyView.run {
+            emptyListViewHeadline.text = headline
+            emptyListViewText.setText(message)
+            emptyListIcon.setImageResource(R.drawable.file_movie)
+            emptyListViewText.visibility = View.VISIBLE
+            emptyListIcon.visibility = View.VISIBLE
+            binding.progress.visibility = View.GONE
+            emptyListView.visibility = View.VISIBLE
+        }
+    }
+
+    /**
+     * tries to read the cover art from the audio file and sets it as cover art.
+     *
+     * @param file audio file with potential cover art
+     */
+    @Suppress("TooGenericExceptionCaught", "NestedBlockDepth")
+    private fun extractAndSetCoverArt(file: OCFile) {
+        if (!MimeTypeUtil.isAudio(file)) {
+            return
+        }
+
+        val bitmap = if (file.storagePath == null) {
+            getAudioThumbnail(file)
+        } else {
+            getThumbnail(file.storagePath) ?: getAudioThumbnail(file)
+        }
+
+        if (bitmap != null) {
+            binding.imagePreview.setImageBitmap(bitmap)
+        } else {
+            setGenericThumbnail()
+        }
+    }
+
+    @Suppress("TooGenericExceptionCaught")
+    private fun getThumbnail(storagePath: String?): Bitmap? {
+        return try {
+            MediaMetadataRetriever().run {
+                setDataSource(storagePath)
+                BitmapFactory.decodeByteArray(embeddedPicture, 0, embeddedPicture?.size ?: 0)
+            }
+        } catch (t: Throwable) {
+            BitmapUtils.drawableToBitmap(genericThumbnail())
+        }
+    }
+
+    private fun getAudioThumbnail(file: OCFile): Bitmap? {
+        return ThumbnailsCacheManager.getBitmapFromDiskCache(
+            ThumbnailsCacheManager.PREFIX_THUMBNAIL + file.remoteId
+        )
+    }
+
+    private fun setGenericThumbnail() {
+        binding.imagePreview.setImageDrawable(genericThumbnail())
+    }
+
+    private fun genericThumbnail(): Drawable? {
+        val result = AppCompatResources.getDrawable(this, R.drawable.logo)
+        result?.let {
+            if (!resources.getBoolean(R.bool.is_branded_client)) {
+                DrawableCompat.setTint(it, resources.getColor(R.color.primary, this.theme))
+            }
+        }
+
+        return result
+    }
+
+    override fun onSaveInstanceState(outState: Bundle) {
+        super.onSaveInstanceState(outState)
+        Log_OC.v(TAG, "onSaveInstanceState")
+        outState.let { bundle ->
+            bundle.putParcelable(EXTRA_FILE, file)
+            bundle.putParcelable(EXTRA_USER, user)
+            saveMediaInstanceState(bundle)
+        }
+    }
+
+    private fun saveMediaInstanceState(bundle: Bundle) {
+        bundle.run {
+            if (MimeTypeUtil.isVideo(file) && exoPlayer != null) {
+                exoPlayer?.let {
+                    savedPlaybackPosition = it.currentPosition
+                    autoplay = it.isPlaying
+                }
+                putLong(EXTRA_PLAY_POSITION, savedPlaybackPosition)
+                putBoolean(EXTRA_PLAYING, autoplay)
+            } else if (mediaPlayerServiceConnection != null && mediaPlayerServiceConnection!!.isConnected) {
+                putInt(EXTRA_PLAY_POSITION, mediaPlayerServiceConnection!!.currentPosition)
+                putBoolean(EXTRA_PLAYING, mediaPlayerServiceConnection!!.isPlaying)
+            }
+        }
+    }
+
+    override fun onStart() {
+        super.onStart()
+
+        Log_OC.v(TAG, "onStart")
+
+        if (file != null) {
+            mediaPlayerServiceConnection?.bind()
+
+            if (MimeTypeUtil.isAudio(file)) {
+                setupAudioPlayerServiceConnection()
+            } else if (MimeTypeUtil.isVideo(file)) {
+                if (mediaPlayerServiceConnection?.isConnected == true) {
+                    stopAudio()
+                }
+
+                if (exoPlayer != null) {
+                    playVideo()
+                } else {
+                    initNextcloudExoPlayer()
+                }
+            }
+        }
+    }
+
+    private fun setupAudioPlayerServiceConnection() {
+        binding.mediaController.run {
+            setMediaPlayer(mediaPlayerServiceConnection)
+            visibility = View.VISIBLE
+        }
+
+        user?.let {
+            mediaPlayerServiceConnection?.start(it, file, autoplay, savedPlaybackPosition)
+        }
+
+        binding.emptyView.emptyListView.visibility = View.GONE
+        binding.progress.visibility = View.GONE
+    }
+
+    private fun initNextcloudExoPlayer() {
+        val handler = Handler(Looper.getMainLooper())
+        Executors.newSingleThreadExecutor().execute {
+            try {
+                nextcloudClient = clientFactory.createNextcloudClient(accountManager.user)
+
+                nextcloudClient?.let { client ->
+                    handler.post {
+                        exoPlayer = createNextcloudExoplayer(this, client)
+
+                        exoPlayer?.let { player ->
+                            player.addListener(
+                                ExoplayerListener(
+                                    this,
+                                    binding.exoplayerView,
+                                    player
+                                )
+                            )
+
+                            playVideo()
+                        }
+                    }
+                }
+            } catch (e: CreationException) {
+                handler.post { Log_OC.e(TAG, "error setting up ExoPlayer", e) }
+            }
+        }
+    }
+
+    private fun initWindowInsetsController() {
+        windowInsetsController = WindowCompat.getInsetsController(
+            window,
+            window.decorView
+        ).apply {
+            systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
+        }
+    }
+
+    private fun applyWindowInsets() {
+        val playerView = binding.exoplayerView
+        val exoControls = playerView.findViewById<FrameLayout>(R.id.exo_bottom_bar)
+        val exoProgress = playerView.findViewById<DefaultTimeBar>(R.id.exo_progress)
+        val progressBottomMargin = exoProgress.marginBottom
+
+        ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _, windowInsets ->
+            val insets = windowInsets.getInsets(
+                WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type
+                    .displayCutout()
+            )
+
+            binding.materialToolbar.updateLayoutParams<ViewGroup.MarginLayoutParams> {
+                topMargin = insets.top
+            }
+            exoControls.updateLayoutParams<ViewGroup.MarginLayoutParams> {
+                bottomMargin = insets.bottom
+            }
+            exoProgress.updateLayoutParams<ViewGroup.MarginLayoutParams> {
+                bottomMargin = insets.bottom + progressBottomMargin
+            }
+            exoControls.updatePadding(left = insets.left, right = insets.right)
+            exoProgress.updatePadding(left = insets.left, right = insets.right)
+            binding.materialToolbar.updatePadding(left = insets.left, right = insets.right)
+            WindowInsetsCompat.CONSUMED
+        }
+    }
+
+    @OptIn(UnstableApi::class)
+    private fun setupVideoView() {
+        initWindowInsetsController()
+        val type = WindowInsetsCompat.Type.systemBars()
+        binding.exoplayerView.let {
+            it.setShowNextButton(false)
+            it.setShowPreviousButton(false)
+            it.setControllerVisibilityListener(
+                PlayerView.ControllerVisibilityListener { visibility ->
+                    if (visibility == View.VISIBLE) {
+                        windowInsetsController.show(type)
+                        supportActionBar!!.show()
+                    } else if (visibility == View.GONE) {
+                        windowInsetsController.hide(type)
+                        supportActionBar!!.hide()
+                    }
+                }
+            )
+            it.player = exoPlayer
+        }
+    }
+
+    private fun stopAudio() {
+        mediaPlayerServiceConnection?.stop()
+    }
+
+    override fun onCreateOptionsMenu(menu: Menu?): Boolean {
+        menuInflater.inflate(R.menu.custom_menu_placeholder, menu)
+        return true
+    }
+
+    override fun onOptionsItemSelected(item: MenuItem): Boolean {
+        if (item.itemId == android.R.id.home) {
+            finish()
+            return true
+        }
+
+        if (item.itemId == R.id.custom_menu_placeholder_item) {
+            val file = file
+
+            if (storageManager != null && file != null) {
+                val updatedFile = storageManager.getFileById(file.fileId)
+                setFile(updatedFile)
+                val fileNew = getFile()
+                fileNew?.let { showFileActions(it) }
+            }
+        }
+
+        return super.onOptionsItemSelected(item)
+    }
+
+    private fun showFileActions(file: OCFile) {
+        val additionalFilter: MutableList<Int> =
+            mutableListOf(
+                R.id.action_rename_file,
+                R.id.action_sync_file,
+                R.id.action_move_or_copy,
+                R.id.action_favorite,
+                R.id.action_unset_favorite,
+                R.id.action_pin_to_homescreen
+            )
+
+        if (getFile() != null && getFile().isSharedWithMe && !getFile().canReshare()) {
+            additionalFilter.add(R.id.action_send_share_file)
+        }
+
+        newInstance(file, false, additionalFilter)
+            .setResultListener(
+                supportFragmentManager,
+                this,
+                object : ResultListener {
+                    override fun onResult(actionId: Int) {
+                        onFileActionChosen(actionId)
+                    }
+                }
+            )
+            .show(supportFragmentManager, "actions")
+    }
+
+    private fun onFileActionChosen(itemId: Int) {
+        when (itemId) {
+            R.id.action_send_share_file -> {
+                sendShareFile()
+            }
+
+            R.id.action_open_file_with -> {
+                openFile()
+            }
+
+            R.id.action_remove_file -> {
+                val dialog = RemoveFilesDialogFragment.newInstance(file)
+                dialog.show(supportFragmentManager, ConfirmationDialogFragment.FTAG_CONFIRMATION)
+            }
+
+            R.id.action_see_details -> {
+                seeDetails()
+            }
+
+            R.id.action_sync_file -> {
+                fileOperationsHelper.syncFile(file)
+            }
+
+            R.id.action_cancel_sync -> {
+                fileOperationsHelper.cancelTransference(file)
+            }
+
+            R.id.action_stream_media -> {
+                fileOperationsHelper.streamMediaFile(file)
+            }
+
+            R.id.action_export_file -> {
+                val list = ArrayList<OCFile>()
+                list.add(file)
+                fileOperationsHelper.exportFiles(
+                    list,
+                    this,
+                    binding.root,
+                    backgroundJobManager
+                )
+            }
+
+            R.id.action_download_file -> {
+                requestForDownload(file, null)
+            }
+        }
+    }
+
+    override fun onRemoteOperationFinish(operation: RemoteOperation<*>?, result: RemoteOperationResult<*>?) {
+        super.onRemoteOperationFinish(operation, result)
+        if (operation is RemoveFileOperation) {
+            DisplayUtils.showSnackMessage(this, ErrorMessageAdapter.getErrorCauseMessage(result, operation, resources))
+
+            val removedFile = operation.file
+            val fileAvailable: Boolean = storageManager.fileExists(removedFile.fileId)
+            if (!fileAvailable && removedFile == file) {
+                finish()
+            }
+        } else if (operation is SynchronizeFileOperation) {
+            onSynchronizeFileOperationFinish(result)
+        }
+    }
+
+    override fun newTransferenceServiceConnection(): ServiceConnection {
+        return PreviewMediaServiceConnection()
+    }
+
+    private fun onSynchronizeFileOperationFinish(result: RemoteOperationResult<*>?) {
+        result?.let {
+            invalidateOptionsMenu()
+        }
+    }
+
+    private inner class PreviewMediaServiceConnection : ServiceConnection {
+        override fun onServiceConnected(componentName: ComponentName?, service: IBinder?) {
+            componentName?.let {
+                if (it == ComponentName(this@PreviewMediaActivity, FileDownloader::class.java)) {
+                    mDownloaderBinder = service as FileDownloaderBinder
+                }
+            }
+        }
+
+        override fun onServiceDisconnected(componentName: ComponentName?) {
+            if (componentName == ComponentName(this@PreviewMediaActivity, FileDownloader::class.java)) {
+                Log_OC.d(PreviewImageActivity.TAG, "Download service suddenly disconnected")
+                mDownloaderBinder = null
+            }
+        }
+    }
+
+    override fun downloadFile(file: OCFile?, packageName: String?, activityName: String?) {
+        requestForDownload(file, OCFileListFragment.DOWNLOAD_SEND, packageName, activityName)
+    }
+
+    private fun requestForDownload(
+        file: OCFile?,
+        downloadBehavior: String? = null,
+        packageName: String? = null,
+        activityName: String? = null
+    ) {
+        if (fileDownloaderBinder.isDownloading(user, file)) {
+            return
+        }
+
+        val intent = Intent(this, FileDownloader::class.java).apply {
+            putExtra(FileDownloader.EXTRA_USER, user)
+            putExtra(FileDownloader.EXTRA_FILE, file)
+            downloadBehavior?.let { behavior ->
+                putExtra(OCFileListFragment.DOWNLOAD_BEHAVIOUR, behavior)
+            }
+            putExtra(SendShareDialog.PACKAGE_NAME, packageName)
+            putExtra(SendShareDialog.ACTIVITY_NAME, activityName)
+        }
+
+        startService(intent)
+    }
+
+    private fun seeDetails() {
+        stopPreview(false)
+        showDetails(file)
+    }
+
+    private fun sendShareFile() {
+        stopPreview(false)
+        fileOperationsHelper.sendShareFile(file)
+    }
+
+    @Suppress("TooGenericExceptionCaught")
+    private fun playVideo() {
+        setupVideoView()
+
+        if (file.isDown) {
+            playVideoUri(file.storageUri)
+        } else {
+            try {
+                LoadStreamUrl(this, user, clientFactory).execute(file.localId)
+            } catch (e: Exception) {
+                Log_OC.e(TAG, "Loading stream url not possible: $e")
+            }
+        }
+    }
+
+    private fun playVideoUri(uri: Uri) {
+        binding.progress.visibility = View.GONE
+
+        exoPlayer?.run {
+            setMediaItem(MediaItem.fromUri(uri))
+            playWhenReady = autoplay
+            prepare()
+
+            if (savedPlaybackPosition >= 0) {
+                seekTo(savedPlaybackPosition)
+            }
+        }
+
+        autoplay = false
+    }
+
+    private class LoadStreamUrl(
+        previewMediaActivity: PreviewMediaActivity,
+        private val user: User?,
+        private val clientFactory: ClientFactory?
+    ) : AsyncTask<Long?, Void?, Uri?>() {
+        private val previewMediaActivityWeakReference: WeakReference<PreviewMediaActivity> =
+            WeakReference(previewMediaActivity)
+
+        @Deprecated("Deprecated in Java")
+        override fun doInBackground(vararg fileId: Long?): Uri? {
+            val client: OwnCloudClient? = try {
+                clientFactory?.create(user)
+            } catch (e: CreationException) {
+                Log_OC.e(TAG, "Loading stream url not possible: $e")
+                return null
+            }
+
+            val sfo = StreamMediaFileOperation(fileId[0]!!)
+            val result = sfo.execute(client)
+
+            return if (!result.isSuccess) {
+                null
+            } else {
+                Uri.parse(result.data[0] as String)
+            }
+        }
+
+        @Deprecated("Deprecated in Java")
+        override fun onPostExecute(uri: Uri?) {
+            val weakReference = previewMediaActivityWeakReference.get()
+            weakReference?.apply {
+                if (uri != null) {
+                    videoUri = uri
+                    playVideoUri(uri)
+                } else {
+                    emptyListView?.visibility = View.VISIBLE
+                    setVideoErrorMessage(
+                        weakReference.getString(R.string.stream_not_possible_headline),
+                        R.string.stream_not_possible_message
+                    )
+                }
+            }
+        }
+    }
+
+    override fun onPause() {
+        Log_OC.v(TAG, "onPause")
+
+        super.onPause()
+    }
+
+    override fun onResume() {
+        super.onResume()
+
+        Log_OC.v(TAG, "onResume")
+    }
+
+    override fun onDestroy() {
+        Log_OC.v(TAG, "onDestroy")
+
+        super.onDestroy()
+
+        exoPlayer?.run {
+            stop()
+            release()
+        }
+    }
+
+    override fun onStop() {
+        Log_OC.v(TAG, "onStop")
+
+        val file = file
+        if (MimeTypeUtil.isAudio(file) && mediaPlayerServiceConnection?.isPlaying == false) {
+            stopAudio()
+        } else if (MimeTypeUtil.isVideo(file) && exoPlayer != null && exoPlayer?.isPlaying == true) {
+            savedPlaybackPosition = exoPlayer?.currentPosition ?: 0L
+            exoPlayer?.pause()
+        }
+
+        mediaPlayerServiceConnection?.unbind()
+
+        super.onStop()
+    }
+
+    override fun showDetails(file: OCFile?) {
+        val intent = Intent(this, FileDisplayActivity::class.java).apply {
+            action = FileDisplayActivity.ACTION_DETAILS
+            putExtra(FileActivity.EXTRA_FILE, file)
+            addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
+        }
+
+        startActivity(intent)
+        finish()
+    }
+
+    override fun showDetails(file: OCFile?, activeTab: Int) {
+        showDetails(file)
+    }
+
+    override fun onBrowsedDownTo(folder: OCFile?) {
+        // TODO Auto-generated method stub
+    }
+
+    override fun onTransferStateChanged(file: OCFile?, downloading: Boolean, uploading: Boolean) {
+        // TODO Auto-generated method stub
+    }
+
+    override fun onConfigurationChanged(newConfig: Configuration) {
+        super.onConfigurationChanged(newConfig)
+        Log_OC.v(TAG, "onConfigurationChanged $this")
+    }
+
+    @Suppress("DEPRECATION")
+    @Deprecated("Deprecated in Java")
+    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+        Log_OC.v(TAG, "onActivityResult $this")
+        super.onActivityResult(requestCode, resultCode, data)
+
+        if (resultCode == Activity.RESULT_OK) {
+            savedPlaybackPosition = data?.getLongExtra(EXTRA_START_POSITION, 0) ?: 0
+            autoplay = data?.getBooleanExtra(EXTRA_AUTOPLAY, false) ?: false
+        }
+    }
+
+    /**
+     * Opens the previewed file with an external application.
+     */
+    private fun openFile() {
+        stopPreview(true)
+        fileOperationsHelper.openFile(file)
+    }
+
+    private fun stopPreview(stopAudio: Boolean) {
+        if (MimeTypeUtil.isAudio(file) && stopAudio) {
+            mediaPlayerServiceConnection?.pause()
+        } else if (MimeTypeUtil.isVideo(file)) {
+            savedPlaybackPosition = exoPlayer?.currentPosition ?: 0
+            exoPlayer?.stop()
+        }
+    }
+
+    val position: Long
+        get() {
+            if (prepared) {
+                savedPlaybackPosition = exoPlayer?.currentPosition ?: 0
+            }
+            Log_OC.v(TAG, "getting position: $savedPlaybackPosition")
+            return savedPlaybackPosition
+        }
+
+    companion object {
+        private val TAG = PreviewMediaActivity::class.java.simpleName
+        const val EXTRA_FILE = "FILE"
+        const val EXTRA_USER = "USER"
+        const val EXTRA_AUTOPLAY = "AUTOPLAY"
+        const val EXTRA_START_POSITION = "START_POSITION"
+        private const val EXTRA_PLAY_POSITION = "PLAY_POSITION"
+        private const val EXTRA_PLAYING = "PLAYING"
+        private const val FILE = "FILE"
+        private const val USER = "USER"
+        private const val PLAYBACK_POSITION = "PLAYBACK_POSITION"
+        private const val AUTOPLAY = "AUTOPLAY"
+
+        /**
+         * Helper method to test if an [OCFile] can be passed to a [PreviewMediaActivity] to be previewed.
+         *
+         * @param file File to test if can be previewed.
+         * @return 'True' if the file can be handled by the activity.
+         */
+        fun canBePreviewed(file: OCFile?): Boolean {
+            return file != null && (MimeTypeUtil.isAudio(file) || MimeTypeUtil.isVideo(file))
+        }
+    }
+}

+ 13 - 33
app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaFragment.java

@@ -5,10 +5,13 @@
  *   @author Chris Narkiewicz
  *   @author Andy Scherzinger
  *   @author TSI-mc
+ *   @author Parneet Singh
+ *
  *   Copyright (C) 2016 ownCloud Inc.
  *   Copyright (C) 2019 Chris Narkiewicz <hello@ezaquarii.com>
  *   Copyright (C) 2020 Andy Scherzinger
  *   Copyright (C) 2023 TSI-mc
+ *   Copyright (C) 2023 Parneet Singh
  *
  *   This program is free software: you can redistribute it and/or modify
  *   it under the terms of the GNU General Public License version 2,
@@ -31,7 +34,6 @@ import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
-import android.graphics.Color;
 import android.graphics.drawable.Drawable;
 import android.media.MediaMetadataRetriever;
 import android.net.Uri;
@@ -47,12 +49,7 @@ import android.view.MotionEvent;
 import android.view.View;
 import android.view.View.OnTouchListener;
 import android.view.ViewGroup;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
 
-import com.google.android.exoplayer2.ExoPlayer;
-import com.google.android.exoplayer2.MediaItem;
-import com.google.android.exoplayer2.ui.StyledPlayerControlView;
 import com.nextcloud.client.account.User;
 import com.nextcloud.client.account.UserAccountManager;
 import com.nextcloud.client.di.Injectable;
@@ -88,13 +85,16 @@ import java.util.concurrent.Executors;
 import javax.inject.Inject;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.OptIn;
 import androidx.annotation.StringRes;
 import androidx.appcompat.content.res.AppCompatResources;
-import androidx.appcompat.widget.AppCompatImageButton;
 import androidx.core.graphics.drawable.DrawableCompat;
 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;
 
 /**
  * This fragment shows a preview of a downloaded media file (audio or video).
@@ -106,7 +106,7 @@ import androidx.fragment.app.FragmentManager;
  * instantiation too.
  */
 public class PreviewMediaFragment extends FileFragment implements OnTouchListener,
-    Injectable, StyledPlayerControlView.OnFullScreenModeChangedListener {
+    Injectable {
 
     private static final String TAG = PreviewMediaFragment.class.getSimpleName();
 
@@ -374,9 +374,7 @@ public class PreviewMediaFragment extends FileFragment implements OnTouchListene
                                 playVideo();
                             });
                         } catch (ClientFactory.CreationException e) {
-                            handler.post(() -> {
-                                Log_OC.e(TAG, "error setting up ExoPlayer", e);
-                            });
+                            handler.post(() -> Log_OC.e(TAG, "error setting up ExoPlayer", e));
                         }
                     });
                 }
@@ -400,25 +398,12 @@ public class PreviewMediaFragment extends FileFragment implements OnTouchListene
             activity.toggleActionBarVisibility(false);
         }
     }
-
+    @OptIn(markerClass = UnstableApi.class)
     private void setupVideoView() {
+        binding.exoplayerView.setShowNextButton(false);
+        binding.exoplayerView.setShowPreviousButton(false);
         binding.exoplayerView.setPlayer(exoPlayer);
-        LinearLayout linearLayout = binding.exoplayerView.findViewById(R.id.exo_center_controls);
-
-        if (linearLayout.getChildCount() == 5) {
-            AppCompatImageButton fullScreenButton = new AppCompatImageButton(requireContext());
-            fullScreenButton.setImageResource(R.drawable.exo_styled_controls_fullscreen_exit);
-            fullScreenButton.setLayoutParams(new LinearLayout.LayoutParams(143, 143));
-            fullScreenButton.setScaleType(ImageView.ScaleType.FIT_CENTER);
-            fullScreenButton.setBackgroundColor(Color.TRANSPARENT);
-
-            fullScreenButton.setOnClickListener(l -> {
-                startFullScreenVideo();
-            });
-
-            linearLayout.addView(fullScreenButton);
-            linearLayout.invalidate();
-        }
+        binding.exoplayerView.setFullscreenButtonClickListener(isFullScreen -> startFullScreenVideo());
     }
 
     private void stopAudio() {
@@ -551,11 +536,6 @@ public class PreviewMediaFragment extends FileFragment implements OnTouchListene
         autoplay = false;
     }
 
-    @Override
-    public void onFullScreenModeChanged(boolean isFullScreen) {
-        Log_OC.e(TAG, "Fullscreen: " + isFullScreen);
-    }
-
     private static class LoadStreamUrl extends AsyncTask<Long, Void, Uri> {
 
         private final ClientFactory clientFactory;

+ 10 - 37
app/src/main/java/com/owncloud/android/ui/preview/PreviewVideoFullscreenDialog.kt

@@ -25,19 +25,18 @@ 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.annotation.OptIn
 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 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.R
 import com.owncloud.android.databinding.DialogPreviewVideoBinding
 import com.owncloud.android.lib.common.utils.Log_OC
 
@@ -49,15 +48,16 @@ import com.owncloud.android.lib.common.utils.Log_OC
  * @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: StyledPlayerView
+    private val sourceView: PlayerView
 ) : Dialog(sourceView.context, android.R.style.Theme_Black_NoTitleBar_Fullscreen) {
 
     private val binding: DialogPreviewVideoBinding = DialogPreviewVideoBinding.inflate(layoutInflater)
-    private var playingStateListener: Player.Listener? = null
+    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.
@@ -112,11 +112,10 @@ class PreviewVideoFullscreenDialog(
         setOnShowListener {
             enableImmersiveMode()
             switchTargetViewFromSource()
-            setListeners()
+            binding.videoPlayer.setFullscreenButtonClickListener { onBackPressed() }
             if (isPlaying) {
                 mExoPlayer.play()
             }
-            binding.videoPlayer.showController()
         }
         super.show()
     }
@@ -125,36 +124,10 @@ class PreviewVideoFullscreenDialog(
         if (shouldUseRotatedVideoWorkaround) {
             mExoPlayer.seekTo(sourceExoPlayer.currentPosition)
         } else {
-            StyledPlayerView.switchTargetView(sourceExoPlayer, sourceView, binding.videoPlayer)
+            PlayerView.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
-
-        // Run once to set initial state of play or pause buttons
-        playListener.onIsPlayingChanged(sourceExoPlayer.isPlaying)
-    }
-
     override fun onBackPressed() {
         val isPlaying = mExoPlayer.isPlaying
         if (isPlaying) {
@@ -178,7 +151,7 @@ class PreviewVideoFullscreenDialog(
         if (shouldUseRotatedVideoWorkaround) {
             sourceExoPlayer.seekTo(mExoPlayer.currentPosition)
         } else {
-            StyledPlayerView.switchTargetView(sourceExoPlayer, binding.videoPlayer, sourceView)
+            PlayerView.switchTargetView(sourceExoPlayer, binding.videoPlayer, sourceView)
         }
     }
 

+ 96 - 0
app/src/main/res/layout/activity_preview_media.xml

@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ownCloud Android client application
+
+  Copyright (C) 2020 Andy Scherzinger
+  Copyright (C) 2015 ownCloud Inc.
+  Copyright (C) 2023 Parneet Singh
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License version 2,
+  as published by the Free Software Foundation.
+
+  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 General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+-->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/top"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:gravity="center"
+    tools:context=".ui.preview.PreviewMediaActivity">
+
+    <com.google.android.material.appbar.MaterialToolbar
+        android:id="@+id/material_toolbar"
+        android:layout_width="match_parent"
+        android:layout_height="?attr/actionBarSize"
+        android:elevation="@dimen/standard_quarter_margin"
+        android:layout_alignParentStart="true"
+        android:layout_alignParentTop="true"
+        android:layout_alignParentEnd="true" />
+
+    <ImageView
+        android:id="@+id/image_preview"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_gravity="center"
+        android:layout_margin="@dimen/standard_margin"
+        android:contentDescription="@string/preview_image_description"
+        android:src="@drawable/logo" />
+
+
+    <androidx.media3.ui.PlayerView
+        android:id="@+id/exoplayer_view"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_gravity="center"
+        app:show_buffering="always" />
+
+    <com.owncloud.android.media.MediaControlView
+        android:id="@+id/media_controller"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_alignParentBottom="true"
+        android:layout_margin="@dimen/standard_margin"
+        android:visibility="gone" />
+
+    <FrameLayout
+        android:id="@+id/progress"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+
+        <com.elyeproj.loaderviewlibrary.LoaderImageView
+            android:layout_width="@dimen/empty_list_icon_layout_width"
+            android:layout_height="@dimen/empty_list_icon_layout_width"
+            android:layout_gravity="center"
+            android:contentDescription="@null"
+            app:corners="24" />
+
+        <ImageView
+            android:layout_width="@dimen/empty_list_icon_layout_width"
+            android:layout_height="@dimen/empty_list_icon_layout_height"
+            android:layout_gravity="center"
+            android:contentDescription="@null"
+            android:padding="@dimen/standard_half_padding"
+            android:src="@drawable/file_movie"
+            app:tint="@color/bg_default" />
+    </FrameLayout>
+
+    <FrameLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+
+        <include
+            android:id="@+id/empty_view"
+            layout="@layout/empty_list" />
+    </FrameLayout>
+
+</RelativeLayout>

+ 2 - 3
app/src/main/res/layout/dialog_preview_video.xml

@@ -18,12 +18,11 @@
   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/>.
 -->
-<com.google.android.exoplayer2.ui.StyledPlayerView xmlns:android="http://schemas.android.com/apk/res/android"
+<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"
-    app:controller_layout_id="@layout/exo_player_control_view" />
+    app:show_buffering="always" />

+ 0 - 110
app/src/main/res/layout/exo_player_control_view.xml

@@ -1,110 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?><!-- Copyright (C) 2016 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:tools="http://schemas.android.com/tools"
-    android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    android:layout_gravity="bottom"
-    android:layoutDirection="ltr"
-    android:background="#CC000000"
-    android:orientation="vertical"
-    tools:targetApi="28">
-
-    <LinearLayout
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:gravity="center"
-        android:paddingTop="4dp"
-        android:orientation="horizontal">
-
-        <ImageButton
-            android:id="@id/exo_prev"
-            style="@style/FullScreenExoControlButton"
-            android:background="?attr/selectableItemBackgroundBorderless"
-            android:src="@drawable/exo_controls_previous"
-            android:contentDescription="@string/exo_controls_previous_description" />
-
-        <ImageButton
-            android:id="@id/exo_rew"
-            style="@style/FullScreenExoControlButton"
-            android:contentDescription="@string/exo_controls_rewind_description"
-            android:src="@drawable/exo_controls_rewind" />
-
-        <ImageButton
-            android:id="@id/exo_play"
-            style="@style/FullScreenExoControlButton"
-            android:contentDescription="@string/exo_controls_play_description"
-            android:src="@drawable/exo_controls_play"
-            android:visibility="gone"
-            tools:visibility="visible" />
-
-        <ImageButton
-            android:id="@id/exo_pause"
-            style="@style/FullScreenExoControlButton"
-            android:contentDescription="@string/exo_controls_pause_description"
-            android:src="@drawable/exo_controls_pause" />
-
-        <ImageButton
-            android:id="@id/exo_ffwd"
-            style="@style/FullScreenExoControlButton"
-            android:contentDescription="@string/exo_controls_fastforward_description"
-            android:src="@drawable/exo_controls_fastforward" />
-
-        <ImageButton
-            android:id="@+id/exo_exit_fs"
-            style="@style/FullScreenExoControlButton"
-            android:contentDescription="@string/exo_controls_fullscreen_exit_description"
-            android:src="@drawable/exo_styled_controls_fullscreen_exit" />
-
-    </LinearLayout>
-
-    <LinearLayout
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:layout_marginTop="4dp"
-        android:gravity="center_vertical"
-        android:orientation="horizontal">
-
-        <TextView
-            android:id="@id/exo_position"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:textSize="14sp"
-            android:textStyle="bold"
-            android:layout_marginStart="@dimen/standard_margin"
-            android:paddingHorizontal="4dp"
-            android:includeFontPadding="false"
-            android:textColor="#FFBEBEBE" />
-
-        <View
-            android:id="@id/exo_progress_placeholder"
-            android:layout_width="0dp"
-            android:layout_weight="1"
-            android:layout_height="26dp" />
-
-        <TextView
-            android:id="@id/exo_duration"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:textSize="14sp"
-            android:textStyle="bold"
-            android:layout_marginEnd="@dimen/standard_margin"
-            android:paddingHorizontal="4dp"
-            android:includeFontPadding="false"
-            android:textColor="#FFBEBEBE" />
-
-    </LinearLayout>
-
-</LinearLayout>

+ 5 - 4
app/src/main/res/layout/files.xml

@@ -21,14 +21,15 @@
     xmlns:tools="http://schemas.android.com/tools"
     android:id="@+id/drawer_layout"
     android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:fitsSystemWindows="true">
+    android:layout_height="match_parent">
 
     <androidx.coordinatorlayout.widget.CoordinatorLayout
         android:layout_width="match_parent"
         android:layout_height="match_parent">
 
-        <include android:id="@+id/appbar" layout="@layout/toolbar_standard" />
+        <include
+            android:id="@+id/appbar"
+            layout="@layout/toolbar_standard" />
 
         <!-- The main content view -->
         <LinearLayout
@@ -62,7 +63,7 @@
             android:visibility="gone"
             app:layout_behavior="com.google.android.material.behavior.HideBottomViewOnScrollBehavior"
             app:srcCompat="@drawable/ic_plus"
-            tools:visibility="visible"/>
+            tools:visibility="visible" />
 
     </androidx.coordinatorlayout.widget.CoordinatorLayout>
 

+ 6 - 7
app/src/main/res/layout/fragment_preview_media.xml

@@ -1,9 +1,9 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
+<?xml version="1.0" encoding="utf-8"?><!--
   ownCloud Android client application
 
   Copyright (C) 2020 Andy Scherzinger
   Copyright (C) 2015 ownCloud Inc.
+  Copyright (C) 2023 Parneet Singh
 
   This program is free software: you can redistribute it and/or modify
   it under the terms of the GNU General Public License version 2,
@@ -20,8 +20,8 @@
 -->
 
 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:tools="http://schemas.android.com/tools"
     xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
     android:id="@+id/top"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
@@ -38,13 +38,12 @@
         android:src="@drawable/logo" />
 
 
-    <com.google.android.exoplayer2.ui.StyledPlayerView
+    <androidx.media3.ui.PlayerView
         android:id="@+id/exoplayer_view"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:layout_gravity="center"
-        app:show_buffering="always"
-        app:show_next_button="false" />
+        app:show_buffering="always" />
 
     <com.owncloud.android.media.MediaControlView
         android:id="@+id/media_controller"
@@ -69,9 +68,9 @@
         <ImageView
             android:layout_width="@dimen/empty_list_icon_layout_width"
             android:layout_height="@dimen/empty_list_icon_layout_height"
-            android:padding="@dimen/standard_half_padding"
             android:layout_gravity="center"
             android:contentDescription="@null"
+            android:padding="@dimen/standard_half_padding"
             android:src="@drawable/file_movie"
             app:tint="@color/bg_default" />
 

+ 5 - 2
app/src/main/res/values-v27/styles.xml

@@ -1,5 +1,4 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
+<?xml version="1.0" encoding="utf-8"?><!--
   Nextcloud Android client application
 
   Copyright (C) 2020 Nextcloud
@@ -48,4 +47,8 @@
         <item name="android:windowLightNavigationBar">false</item>
     </style>
 
+    <style name="Theme.ownCloud.Media" parent="Theme.Material3.DayNight.NoActionBar">
+        <item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
+    </style>
+
 </resources>

+ 0 - 4
app/src/main/res/values/styles.xml

@@ -454,10 +454,6 @@
         <item name="android:textStyle">bold</item>
     </style>
 
-    <style name="FullScreenExoControlButton" parent="ExoStyledControls.Button.Center">
-        <item name="android:background">@drawable/ripple</item>
-    </style>
-
     <style name="Widget.Nextcloud.AppWidget.Container" parent="android:Widget">
         <item name="android:id">@android:id/background</item>
         <item name="android:background">?android:attr/colorBackground</item>