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