diff --git a/app/build.gradle b/app/build.gradle
index dd57414dda..9a67f4f7f3 100644
--- a/app/build.gradle
+++ b/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'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index f9cc7c311c..11fdfa1d94 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -234,6 +234,11 @@
android:name=".ui.preview.PreviewImageActivity"
android:exported="false"
android:theme="@style/Theme.ownCloud.Overlay" />
+
diff --git a/app/src/main/java/com/nextcloud/client/di/ComponentsModule.java b/app/src/main/java/com/nextcloud/client/di/ComponentsModule.java
index 83924bdb8f..e96707176c 100644
--- a/app/src/main/java/com/nextcloud/client/di/ComponentsModule.java
+++ b/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();
diff --git a/app/src/main/java/com/nextcloud/client/media/ErrorFormat.kt b/app/src/main/java/com/nextcloud/client/media/ErrorFormat.kt
index 4c22586248..37cda17186 100644
--- a/app/src/main/java/com/nextcloud/client/media/ErrorFormat.kt
+++ b/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
/**
diff --git a/app/src/main/java/com/nextcloud/client/media/ExoplayerListener.kt b/app/src/main/java/com/nextcloud/client/media/ExoplayerListener.kt
index abefb26162..e1230d618b 100644
--- a/app/src/main/java/com/nextcloud/client/media/ExoplayerListener.kt
+++ b/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
diff --git a/app/src/main/java/com/nextcloud/client/media/NextcloudExoPlayer.kt b/app/src/main/java/com/nextcloud/client/media/NextcloudExoPlayer.kt
index 31e89342fe..9a9678b917 100644
--- a/app/src/main/java/com/nextcloud/client/media/NextcloudExoPlayer.kt
+++ b/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)
diff --git a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java
index dad544bc17..b0b23675dc 100644
--- a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java
+++ b/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 = 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) {
+ 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 {
diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/SetupEncryptionDialogFragment.kt b/app/src/main/java/com/owncloud/android/ui/dialog/SetupEncryptionDialogFragment.kt
index ea904a9678..9a507dfc59 100644
--- a/app/src/main/java/com/owncloud/android/ui/dialog/SetupEncryptionDialogFragment.kt
+++ b/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
diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java
index 11594d5749..ef302fedc1 100644
--- a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java
+++ b/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) {
diff --git a/app/src/main/java/com/owncloud/android/ui/preview/PreviewImagePagerAdapter.java b/app/src/main/java/com/owncloud/android/ui/preview/PreviewImagePagerAdapter.java
index 52a71b4e4a..f09d51f045 100644
--- a/app/src/main/java/com/owncloud/android/ui/preview/PreviewImagePagerAdapter.java
+++ b/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);
}
diff --git a/app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaActivity.kt b/app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaActivity.kt
new file mode 100644
index 0000000000..f9969d2835
--- /dev/null
+++ b/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
+ * 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 .
+ */
+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(R.id.exo_bottom_bar)
+ val exoProgress = playerView.findViewById(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 {
+ topMargin = insets.top
+ }
+ exoControls.updateLayoutParams {
+ bottomMargin = insets.bottom
+ }
+ exoProgress.updateLayoutParams {
+ 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 =
+ 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()
+ 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() {
+ private val previewMediaActivityWeakReference: WeakReference =
+ 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))
+ }
+ }
+}
diff --git a/app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaFragment.java b/app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaFragment.java
index 04a74d310f..baf19af7cb 100644
--- a/app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaFragment.java
+++ b/app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaFragment.java
@@ -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
* 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 {
private final ClientFactory clientFactory;
diff --git a/app/src/main/java/com/owncloud/android/ui/preview/PreviewVideoFullscreenDialog.kt b/app/src/main/java/com/owncloud/android/ui/preview/PreviewVideoFullscreenDialog.kt
index 90407930f5..35893c19da 100644
--- a/app/src/main/java/com/owncloud/android/ui/preview/PreviewVideoFullscreenDialog.kt
+++ b/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(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)
}
}
diff --git a/app/src/main/res/layout/activity_preview_media.xml b/app/src/main/res/layout/activity_preview_media.xml
new file mode 100644
index 0000000000..37a2483820
--- /dev/null
+++ b/app/src/main/res/layout/activity_preview_media.xml
@@ -0,0 +1,96 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/dialog_preview_video.xml b/app/src/main/res/layout/dialog_preview_video.xml
index 445ad570c5..cbae3164dc 100644
--- a/app/src/main/res/layout/dialog_preview_video.xml
+++ b/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 .
-->
-
+ app:show_buffering="always" />
diff --git a/app/src/main/res/layout/exo_player_control_view.xml b/app/src/main/res/layout/exo_player_control_view.xml
deleted file mode 100644
index 9d10692634..0000000000
--- a/app/src/main/res/layout/exo_player_control_view.xml
+++ /dev/null
@@ -1,110 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/files.xml b/app/src/main/res/layout/files.xml
index 1d7a36e51d..afa638a0d0 100644
--- a/app/src/main/res/layout/files.xml
+++ b/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">
-
+
+ tools:visibility="visible" />
diff --git a/app/src/main/res/layout/fragment_preview_media.xml b/app/src/main/res/layout/fragment_preview_media.xml
index ad82ea7773..0f1708b73e 100644
--- a/app/src/main/res/layout/fragment_preview_media.xml
+++ b/app/src/main/res/layout/fragment_preview_media.xml
@@ -1,9 +1,9 @@
-
-
-
+ app:show_buffering="always" />
diff --git a/app/src/main/res/values-v27/styles.xml b/app/src/main/res/values-v27/styles.xml
index a8e0a954ab..3e6b0585d7 100644
--- a/app/src/main/res/values-v27/styles.xml
+++ b/app/src/main/res/values-v27/styles.xml
@@ -1,5 +1,4 @@
-
-