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

Migrate to media3 and Immersive mode for video playback
This commit is contained in:
Alper Öztürk 2023-12-29 09:01:05 +01:00 committed by GitHub
commit 76369d4880
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 1047 additions and 246 deletions

View file

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

View file

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

View file

@ -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();

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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);
}

View file

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

View file

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

View file

@ -25,19 +25,18 @@ package com.owncloud.android.ui.preview
import android.app.Activity
import android.app.Dialog
import android.os.Build
import android.view.View
import android.view.ViewGroup
import android.view.Window
import androidx.annotation.OptIn
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.ui.StyledPlayerView
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.ui.PlayerView
import com.nextcloud.client.media.ExoplayerListener
import com.nextcloud.client.media.NextcloudExoPlayer
import com.nextcloud.common.NextcloudClient
import com.owncloud.android.R
import com.owncloud.android.databinding.DialogPreviewVideoBinding
import com.owncloud.android.lib.common.utils.Log_OC
@ -49,15 +48,16 @@ import com.owncloud.android.lib.common.utils.Log_OC
* @param sourceExoPlayer the ExoPlayer playing the video
* @param sourceView the original non-fullscreen surface that [sourceExoPlayer] is linked to
*/
@OptIn(UnstableApi::class)
class PreviewVideoFullscreenDialog(
private val activity: Activity,
nextcloudClient: NextcloudClient,
private val sourceExoPlayer: ExoPlayer,
private val sourceView: StyledPlayerView
private val sourceView: PlayerView
) : Dialog(sourceView.context, android.R.style.Theme_Black_NoTitleBar_Fullscreen) {
private val binding: DialogPreviewVideoBinding = DialogPreviewVideoBinding.inflate(layoutInflater)
private var playingStateListener: Player.Listener? = null
private var playingStateListener: androidx.media3.common.Player.Listener? = null
/**
* exoPlayer instance used for this view, either the original one or a new one in specific cases.
@ -112,11 +112,10 @@ class PreviewVideoFullscreenDialog(
setOnShowListener {
enableImmersiveMode()
switchTargetViewFromSource()
setListeners()
binding.videoPlayer.setFullscreenButtonClickListener { onBackPressed() }
if (isPlaying) {
mExoPlayer.play()
}
binding.videoPlayer.showController()
}
super.show()
}
@ -125,36 +124,10 @@ class PreviewVideoFullscreenDialog(
if (shouldUseRotatedVideoWorkaround) {
mExoPlayer.seekTo(sourceExoPlayer.currentPosition)
} else {
StyledPlayerView.switchTargetView(sourceExoPlayer, sourceView, binding.videoPlayer)
PlayerView.switchTargetView(sourceExoPlayer, sourceView, binding.videoPlayer)
}
}
private fun setListeners() {
binding.root.findViewById<View>(R.id.exo_exit_fs).setOnClickListener { onBackPressed() }
val pauseButton: View = binding.root.findViewById(R.id.exo_pause)
pauseButton.setOnClickListener { sourceExoPlayer.pause() }
val playButton: View = binding.root.findViewById(R.id.exo_play)
playButton.setOnClickListener { sourceExoPlayer.play() }
val playListener = object : Player.Listener {
override fun onIsPlayingChanged(isPlaying: Boolean) {
super.onIsPlayingChanged(isPlaying)
if (isPlaying) {
playButton.visibility = View.GONE
pauseButton.visibility = View.VISIBLE
} else {
playButton.visibility = View.VISIBLE
pauseButton.visibility = View.GONE
}
}
}
mExoPlayer.addListener(playListener)
playingStateListener = playListener
// Run once to set initial state of play or pause buttons
playListener.onIsPlayingChanged(sourceExoPlayer.isPlaying)
}
override fun onBackPressed() {
val isPlaying = mExoPlayer.isPlaying
if (isPlaying) {
@ -178,7 +151,7 @@ class PreviewVideoFullscreenDialog(
if (shouldUseRotatedVideoWorkaround) {
sourceExoPlayer.seekTo(mExoPlayer.currentPosition)
} else {
StyledPlayerView.switchTargetView(sourceExoPlayer, binding.videoPlayer, sourceView)
PlayerView.switchTargetView(sourceExoPlayer, binding.videoPlayer, sourceView)
}
}

View file

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

View file

@ -18,12 +18,11 @@
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<com.google.android.exoplayer2.ui.StyledPlayerView xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.media3.ui.PlayerView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/videoPlayer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:background="@color/black"
app:show_buffering="always"
app:controller_layout_id="@layout/exo_player_control_view" />
app:show_buffering="always" />

View file

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

View file

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

View file

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

View file

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

View file

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